Single-commit clean baseline after security scrub of niche-tells, project codenames, internal jargon, and contributor-email leaks. Contents: - 100 Rust crates (_primitives/_rust/) - 37 agent manifests (_manifests/) + generated specs (_generated/) - 67 user-invocable skills (skills/) - 33 hooks (hooks/) - Composition blocks (_blocks/) - Documentation (docs/, README.md) - TS adapter packages (_ts_packages/) - Assembler (_assembler/) - Roles (_roles/) - Templates (_templates/) - Forgejo CI (.forgejo/) Author: Denis Parfionovich <info@greendragon.info> License: see LICENSE.
174 lines
5.4 KiB
Rust
174 lines
5.4 KiB
Rust
//! Integration tests for kei-migrate against a SQLite file (safe, no deps).
|
|
//!
|
|
//! SQLite is chosen as the test backend because it has no server dependency
|
|
//! and the sqlx-Any path through it exercises the same code path as Postgres
|
|
//! / MySQL for everything except dialect-specific DDL (which we abstract in
|
|
//! `db::Backend::create_tracker_sql`).
|
|
|
|
use kei_migrate::{cmd_create, db, discover, do_down, do_status, do_up, tracker};
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
use tempfile::TempDir;
|
|
|
|
struct Env {
|
|
_tmp: TempDir,
|
|
url: String,
|
|
dir: PathBuf,
|
|
}
|
|
|
|
fn setup() -> Env {
|
|
let tmp = TempDir::new().unwrap();
|
|
let db_path = tmp.path().join("test.db");
|
|
let url = format!("sqlite://{}?mode=rwc", db_path.display());
|
|
let dir = tmp.path().join("migrations");
|
|
fs::create_dir_all(&dir).unwrap();
|
|
Env { _tmp: tmp, url, dir }
|
|
}
|
|
|
|
fn write_migration(dir: &std::path::Path, version: i64, name: &str, up: &str, down: Option<&str>) {
|
|
fs::write(dir.join(format!("{}_{}.sql", version, name)), up).unwrap();
|
|
if let Some(d) = down {
|
|
fs::write(dir.join(format!("{}_{}.down.sql", version, name)), d).unwrap();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn detects_backend_from_url_scheme() {
|
|
assert_eq!(
|
|
db::detect_backend("postgres://u:p@h/d").unwrap(),
|
|
db::Backend::Postgres
|
|
);
|
|
assert_eq!(
|
|
db::detect_backend("sqlite:///tmp/x.db").unwrap(),
|
|
db::Backend::Sqlite
|
|
);
|
|
assert_eq!(
|
|
db::detect_backend("mysql://u:p@h/d").unwrap(),
|
|
db::Backend::Mysql
|
|
);
|
|
assert!(db::detect_backend("mongodb://h").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn scan_empty_dir_is_empty() {
|
|
let env = setup();
|
|
let migs = discover::scan(&env.dir).unwrap();
|
|
assert!(migs.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn scan_sorts_by_version_and_skips_down_files() {
|
|
let env = setup();
|
|
write_migration(&env.dir, 2, "second", "SELECT 1;", Some("SELECT 2;"));
|
|
write_migration(&env.dir, 1, "first", "SELECT 3;", None);
|
|
let migs = discover::scan(&env.dir).unwrap();
|
|
assert_eq!(migs.len(), 2);
|
|
assert_eq!(migs[0].version, 1);
|
|
assert_eq!(migs[1].version, 2);
|
|
assert!(migs[0].down_path.is_none());
|
|
assert!(migs[1].down_path.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn scan_rejects_duplicate_versions() {
|
|
let env = setup();
|
|
write_migration(&env.dir, 1, "a", "SELECT 1;", None);
|
|
// same version, different name
|
|
fs::write(env.dir.join("1_b.sql"), "SELECT 2;").unwrap();
|
|
let err = discover::scan(&env.dir).unwrap_err();
|
|
assert!(err.to_string().contains("duplicate migration version"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn up_applies_pending_and_tracks_them() {
|
|
let env = setup();
|
|
write_migration(
|
|
&env.dir,
|
|
1,
|
|
"create_t",
|
|
"CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT);",
|
|
Some("DROP TABLE t;"),
|
|
);
|
|
write_migration(
|
|
&env.dir,
|
|
2,
|
|
"insert_row",
|
|
"INSERT INTO t (id, v) VALUES (1, 'a');",
|
|
Some("DELETE FROM t WHERE id = 1;"),
|
|
);
|
|
let n = do_up(&env.url, &env.dir).await.unwrap();
|
|
assert_eq!(n, 2);
|
|
// re-running is a no-op
|
|
let n2 = do_up(&env.url, &env.dir).await.unwrap();
|
|
assert_eq!(n2, 0);
|
|
// status: 2 applied, 0 pending
|
|
let (a, p) = do_status(&env.url, &env.dir).await.unwrap();
|
|
assert_eq!(a, 2);
|
|
assert_eq!(p, 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn down_reverts_last_n() {
|
|
let env = setup();
|
|
write_migration(
|
|
&env.dir,
|
|
1,
|
|
"create_t",
|
|
"CREATE TABLE t (id INTEGER PRIMARY KEY);",
|
|
Some("DROP TABLE t;"),
|
|
);
|
|
write_migration(
|
|
&env.dir,
|
|
2,
|
|
"add_col",
|
|
"ALTER TABLE t ADD COLUMN v TEXT;",
|
|
Some("-- down unsupported on sqlite but we just drop the table for this test\nDROP TABLE t; CREATE TABLE t (id INTEGER PRIMARY KEY);"),
|
|
);
|
|
do_up(&env.url, &env.dir).await.unwrap();
|
|
let reverted = do_down(&env.url, &env.dir, 1).await.unwrap();
|
|
assert_eq!(reverted, 1);
|
|
let (a, p) = do_status(&env.url, &env.dir).await.unwrap();
|
|
assert_eq!(a, 1);
|
|
assert_eq!(p, 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn down_refuses_irreversible_marker() {
|
|
let env = setup();
|
|
write_migration(
|
|
&env.dir,
|
|
1,
|
|
"make_t",
|
|
"CREATE TABLE t (id INTEGER PRIMARY KEY);",
|
|
Some("-- IRREVERSIBLE\n-- don't auto-revert"),
|
|
);
|
|
do_up(&env.url, &env.dir).await.unwrap();
|
|
let err = do_down(&env.url, &env.dir, 1).await.unwrap_err();
|
|
assert!(err.to_string().contains("IRREVERSIBLE"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn up_detects_checksum_drift() {
|
|
let env = setup();
|
|
let up_path = env.dir.join("1_seed.sql");
|
|
fs::write(&up_path, "CREATE TABLE t (id INTEGER PRIMARY KEY);").unwrap();
|
|
do_up(&env.url, &env.dir).await.unwrap();
|
|
// someone edits the already-applied file
|
|
fs::write(&up_path, "CREATE TABLE t (id INTEGER PRIMARY KEY, extra TEXT);").unwrap();
|
|
let err = do_up(&env.url, &env.dir).await.unwrap_err();
|
|
assert!(err.to_string().contains("checksum drift"));
|
|
// tracker module is exercised end-to-end here
|
|
let _ = tracker::applied_versions; // keep tracker import used
|
|
}
|
|
|
|
#[test]
|
|
fn create_scaffolds_up_and_down_files() {
|
|
let env = setup();
|
|
let (up, down) = cmd_create::run(&env.dir, "add_users_index").unwrap();
|
|
assert!(up.exists());
|
|
assert!(down.exists());
|
|
let up_txt = fs::read_to_string(&up).unwrap();
|
|
let down_txt = fs::read_to_string(&down).unwrap();
|
|
assert!(up_txt.contains("up migration"));
|
|
assert!(down_txt.contains("down migration"));
|
|
}
|