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,416 @@
use nomifun_db::{init_database, init_database_memory};
use sqlx::Row;
// -- T1.1 Initialization --
#[tokio::test]
async fn init_creates_users_table() {
let db = init_database_memory().await.unwrap();
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users")
.fetch_one(db.pool())
.await
.unwrap();
assert!(
count.0 >= 1,
"users table should exist and have at least the system user"
);
}
// -- T1.2 Pragma configuration --
#[tokio::test]
async fn pragma_foreign_keys_enabled() {
let db = init_database_memory().await.unwrap();
let row: (i64,) = sqlx::query_as("PRAGMA foreign_keys")
.fetch_one(db.pool())
.await
.unwrap();
assert_eq!(row.0, 1, "foreign_keys should be ON");
}
#[tokio::test]
async fn pragma_busy_timeout() {
let db = init_database_memory().await.unwrap();
let row: (i64,) = sqlx::query_as("PRAGMA busy_timeout")
.fetch_one(db.pool())
.await
.unwrap();
assert_eq!(row.0, 5000, "busy_timeout should be 5000ms");
}
#[tokio::test]
async fn pragma_journal_mode_wal_on_file() {
let dir = tempfile::tempdir().unwrap();
let db = init_database(&dir.path().join("test.db")).await.unwrap();
let row: (String,) = sqlx::query_as("PRAGMA journal_mode")
.fetch_one(db.pool())
.await
.unwrap();
assert_eq!(
row.0.to_lowercase(),
"wal",
"journal_mode should be WAL for file-backed DB"
);
db.close().await;
}
// -- T1.3 Idempotent re-initialization --
#[tokio::test]
async fn idempotent_reinit_preserves_data() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.db");
// First init + insert test data
let db = init_database(&path).await.unwrap();
sqlx::query(
"INSERT INTO users (id, username, password_hash, created_at, updated_at) \
VALUES ('u1', 'alice', 'hash123', 1000, 1000)",
)
.execute(db.pool())
.await
.unwrap();
db.close().await;
// Second init — data should persist
let db = init_database(&path).await.unwrap();
let row = sqlx::query("SELECT username FROM users WHERE id = 'u1'")
.fetch_one(db.pool())
.await
.unwrap();
assert_eq!(row.get::<String, _>("username"), "alice");
db.close().await;
}
// -- T1.4 Migrations --
#[tokio::test]
async fn migrations_applied() {
let db = init_database_memory().await.unwrap();
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM _sqlx_migrations WHERE success = 1")
.fetch_one(db.pool())
.await
.unwrap();
assert!(count.0 >= 1, "at least one migration should be applied");
}
// -- T1.5 System default user --
#[tokio::test]
async fn system_default_user_exists() {
let db = init_database_memory().await.unwrap();
let row = sqlx::query("SELECT id, username, password_hash FROM users WHERE id = 'system_default_user'")
.fetch_one(db.pool())
.await
.unwrap();
assert_eq!(row.get::<String, _>("id"), "system_default_user");
assert_eq!(row.get::<String, _>("username"), "admin");
assert_eq!(
row.get::<String, _>("password_hash"),
"",
"system user should have empty password hash"
);
}
#[tokio::test]
async fn system_user_has_valid_timestamps() {
let before = nomifun_common::now_ms();
let db = init_database_memory().await.unwrap();
let after = nomifun_common::now_ms();
let row = sqlx::query("SELECT created_at, updated_at FROM users WHERE id = 'system_default_user'")
.fetch_one(db.pool())
.await
.unwrap();
let created = row.get::<i64, _>("created_at");
let updated = row.get::<i64, _>("updated_at");
assert!(
created >= before && created <= after,
"created_at should be within test window"
);
assert!(
updated >= before && updated <= after,
"updated_at should be within test window"
);
}
// -- Schema validation --
#[tokio::test]
async fn users_table_accepts_all_columns() {
let db = init_database_memory().await.unwrap();
sqlx::query(
"INSERT INTO users \
(id, username, email, password_hash, avatar_path, jwt_secret, created_at, updated_at, last_login) \
VALUES ('u1', 'testuser', 'test@example.com', 'hash', '/avatar.png', 'secret', 1000, 2000, 3000)",
)
.execute(db.pool())
.await
.unwrap();
let row = sqlx::query("SELECT * FROM users WHERE id = 'u1'")
.fetch_one(db.pool())
.await
.unwrap();
assert_eq!(row.get::<String, _>("email"), "test@example.com");
assert_eq!(
row.get::<Option<String>, _>("avatar_path"),
Some("/avatar.png".to_string())
);
assert_eq!(row.get::<Option<String>, _>("jwt_secret"), Some("secret".to_string()));
assert_eq!(row.get::<Option<i64>, _>("last_login"), Some(3000));
}
#[tokio::test]
async fn username_unique_constraint() {
let db = init_database_memory().await.unwrap();
sqlx::query(
"INSERT INTO users (id, username, password_hash, created_at, updated_at) \
VALUES ('u1', 'duplicate', 'h', 1, 1)",
)
.execute(db.pool())
.await
.unwrap();
let result = sqlx::query(
"INSERT INTO users (id, username, password_hash, created_at, updated_at) \
VALUES ('u2', 'duplicate', 'h', 1, 1)",
)
.execute(db.pool())
.await;
assert!(result.is_err(), "duplicate username should violate unique constraint");
}
#[tokio::test]
async fn email_unique_constraint() {
let db = init_database_memory().await.unwrap();
sqlx::query(
"INSERT INTO users (id, username, email, password_hash, created_at, updated_at) \
VALUES ('u1', 'user1', 'same@example.com', 'h', 1, 1)",
)
.execute(db.pool())
.await
.unwrap();
let result = sqlx::query(
"INSERT INTO users (id, username, email, password_hash, created_at, updated_at) \
VALUES ('u2', 'user2', 'same@example.com', 'h', 1, 1)",
)
.execute(db.pool())
.await;
assert!(result.is_err(), "duplicate email should violate unique constraint");
}
// -- Corruption recovery --
#[tokio::test]
async fn corruption_recovery_creates_backup() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.db");
// Write invalid content to simulate corruption
std::fs::write(&path, b"not a valid sqlite database").unwrap();
let db = init_database(&path).await.unwrap();
// Recovered database should work
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users")
.fetch_one(db.pool())
.await
.unwrap();
assert!(count.0 >= 1, "recovered DB should have system user");
// Backup file should exist
let has_backup = std::fs::read_dir(dir.path())
.unwrap()
.filter_map(|e| e.ok())
.any(|e| e.file_name().to_string_lossy().contains("backup"));
assert!(has_backup, "backup of corrupted file should exist");
db.close().await;
}
// -- Directory creation --
#[tokio::test]
async fn creates_parent_directories() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("sub").join("nested").join("test.db");
let db = init_database(&path).await.unwrap();
assert!(path.exists(), "database file should be created in nested directory");
db.close().await;
}
// -- Pre-baseline rebuild (pre-launch convenience; removed before release) --
//
// The 2026-06-12 clean-baseline refactor squashed migrations 001021 into a
// single 001_baseline.sql, resetting the migration chain. A dev database
// carrying the old `_sqlx_migrations` history (mismatched checksum on
// version 1, applied versions 221 missing from the resolved set) must be
// renamed to `*.pre-baseline.bak` and rebuilt empty instead of failing fast.
#[tokio::test]
async fn pre_baseline_database_is_renamed_and_rebuilt() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("nomifun-backend.db");
// Build a valid database, then forge a pre-baseline migration history:
// tamper the baseline checksum and record extra applied versions.
let db = init_database(&path).await.unwrap();
sqlx::query("UPDATE _sqlx_migrations SET checksum = X'00'")
.execute(db.pool())
.await
.unwrap();
sqlx::query(
"INSERT INTO _sqlx_migrations (version, description, success, checksum, execution_time) \
VALUES (21, 'entity seq', TRUE, X'00', 0)",
)
.execute(db.pool())
.await
.unwrap();
// Marker row that must NOT survive the rebuild.
sqlx::query(
"INSERT INTO users (id, username, password_hash, created_at, updated_at) \
VALUES ('u_old', 'old_dev_user', 'h', 1, 1)",
)
.execute(db.pool())
.await
.unwrap();
db.close().await;
// Re-init: the version mismatch must trigger the rename-and-rebuild path.
let db = init_database(&path).await.unwrap();
let old_user: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users WHERE id = 'u_old'")
.fetch_one(db.pool())
.await
.unwrap();
assert_eq!(old_user.0, 0, "rebuilt DB must be empty (old data renamed aside)");
let system_user: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users WHERE id = 'system_default_user'")
.fetch_one(db.pool())
.await
.unwrap();
assert_eq!(system_user.0, 1, "rebuilt DB should have the system user");
let backup = dir.path().join("nomifun-backend.db.pre-baseline.bak");
assert!(backup.exists(), "old database should be preserved as .pre-baseline.bak");
db.close().await;
}
#[tokio::test]
async fn pre_baseline_rebuild_numbers_subsequent_backups() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("nomifun-backend.db");
// Occupy the primary backup name so the rebuild has to pick a suffix.
std::fs::write(dir.path().join("nomifun-backend.db.pre-baseline.bak"), b"earlier backup").unwrap();
let db = init_database(&path).await.unwrap();
sqlx::query("UPDATE _sqlx_migrations SET checksum = X'00'")
.execute(db.pool())
.await
.unwrap();
db.close().await;
let db = init_database(&path).await.unwrap();
let numbered = dir.path().join("nomifun-backend.db.pre-baseline.bak.1");
assert!(numbered.exists(), "second backup should get a numeric suffix");
db.close().await;
}
// -- Concurrent migrator regression (ELECTRON-1KK) --
//
// Repro for the Sentry secondary symptom: two processes opening the same
// SQLite DB on first start (e.g. Electron auto-update spawning the new
// version while the old one is still finalising shutdown, or
// `nomicore doctor` racing the server) both decide to apply the same
// migration version. sqlx-sqlite's lock()/unlock() are no-ops, so without
// the advisory file lock and retry-on-UNIQUE the slower process used to
// blow up with `UNIQUE constraint failed: _sqlx_migrations.version`.
//
// We use OS threads (not tokio::spawn) so each migrator runs on its own
// runtime — this matches the real "two processes" topology more closely
// than cooperative tasks would, and avoids the `&SqlitePool: Send` lifetime
// gymnastics that block tokio::spawn on this future.
#[test]
fn concurrent_init_database_does_not_panic_on_unique_conflict() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("nomifun-backend.db");
let mut handles = Vec::new();
for _ in 0..8 {
let p = path.clone();
handles.push(std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async move { init_database(&p).await })
}));
}
// Every thread must succeed — none should bubble up the UNIQUE-constraint
// error from `_sqlx_migrations`.
let mut errors = Vec::new();
for h in handles {
match h.join().expect("thread panicked") {
Ok(_db) => {}
Err(e) => errors.push(e.to_string()),
}
}
assert!(
errors.is_empty(),
"all parallel migrators should succeed, got errors: {errors:?}"
);
// All migrators converged on the same baseline schema with no duplicate
// `_sqlx_migrations` rows.
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
let db = init_database(&path).await.unwrap();
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM _sqlx_migrations WHERE success = 1")
.fetch_one(db.pool())
.await
.unwrap();
assert!(count.0 >= 1, "at least one migration should be recorded");
let dup: (i64,) = sqlx::query_as(
"SELECT COUNT(*) FROM (SELECT version FROM _sqlx_migrations GROUP BY version HAVING COUNT(*) > 1)",
)
.fetch_one(db.pool())
.await
.unwrap();
assert_eq!(dup.0, 0, "no duplicate versions should ever exist in _sqlx_migrations");
db.close().await;
});
// Lock file is created next to the DB and is harmless to leave behind.
let lock = path.with_file_name("nomifun-backend.db.migrate.lock");
assert!(lock.exists(), "advisory lock file should be present after migrate");
}