- Sync --all with async concurrency, per-host throttling, failure budgets, resumable execution - External ref bundling at fetch time with origin tracking - Cross-alias discovery (--all-aliases) for list and search commands - CI/CD pipeline (.gitlab-ci.yml), cargo-deny config, Dockerfile, install script - Reliability test suite: crash consistency (8 tests), lock contention (3 tests), property-based (4 tests) - Criterion performance benchmarks (5 benchmarks) - Bug fix: doctor --fix now repairs missing index.json when raw.json exists - Bug fix: shared $ref references no longer incorrectly flagged as circular (refs.rs) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
169 lines
5.8 KiB
Rust
169 lines
5.8 KiB
Rust
//! Crash consistency tests for the cache write protocol.
|
|
//!
|
|
//! Simulates partial writes at each stage of the crash-consistent write protocol
|
|
//! and verifies that the read protocol detects corruption and doctor --fix repairs it.
|
|
|
|
mod helpers;
|
|
|
|
use std::fs;
|
|
|
|
/// Helper: fetch a spec into the test environment so we have valid cache state.
|
|
fn setup_with_cached_spec(env: &helpers::TestEnv) {
|
|
helpers::fetch_fixture(env, "petstore.json", "petstore");
|
|
}
|
|
|
|
/// When raw.json exists but meta.json is missing (crash before commit marker),
|
|
/// load_index should fail with AliasNotFound (meta is the commit marker).
|
|
#[test]
|
|
fn test_crash_before_meta_write() {
|
|
let env = helpers::TestEnv::new();
|
|
setup_with_cached_spec(&env);
|
|
|
|
let alias_dir = env.cache_dir.join("petstore");
|
|
let meta_path = alias_dir.join("meta.json");
|
|
|
|
// Simulate crash: remove meta.json (the commit marker)
|
|
assert!(meta_path.exists(), "meta.json should exist after fetch");
|
|
fs::remove_file(&meta_path).expect("failed to remove meta.json");
|
|
|
|
// Read should fail — no commit marker
|
|
let result = helpers::run_cmd(&env, &["list", "petstore", "--robot"]);
|
|
result.failure();
|
|
}
|
|
|
|
/// When meta.json exists but index.json is corrupted (partial write),
|
|
/// load_index should fail with CacheIntegrity (hash mismatch).
|
|
#[test]
|
|
fn test_crash_corrupted_index() {
|
|
let env = helpers::TestEnv::new();
|
|
setup_with_cached_spec(&env);
|
|
|
|
let alias_dir = env.cache_dir.join("petstore");
|
|
let index_path = alias_dir.join("index.json");
|
|
|
|
// Corrupt the index file
|
|
fs::write(&index_path, b"{ corrupted }").expect("failed to corrupt index");
|
|
|
|
// Read should fail — hash mismatch
|
|
let result = helpers::run_cmd(&env, &["list", "petstore", "--robot"]);
|
|
result.failure();
|
|
}
|
|
|
|
/// When meta.json exists but raw.json is corrupted,
|
|
/// load_raw should fail with CacheIntegrity (hash mismatch).
|
|
/// Note: list/search only need index.json, so we test via show which needs raw.json.
|
|
#[test]
|
|
fn test_crash_corrupted_raw_json() {
|
|
let env = helpers::TestEnv::new();
|
|
setup_with_cached_spec(&env);
|
|
|
|
let alias_dir = env.cache_dir.join("petstore");
|
|
let raw_path = alias_dir.join("raw.json");
|
|
|
|
// Corrupt raw.json
|
|
fs::write(&raw_path, b"{ corrupted }").expect("failed to corrupt raw.json");
|
|
|
|
// show needs raw.json for $ref expansion → should fail
|
|
let result = helpers::run_cmd(
|
|
&env,
|
|
&["show", "petstore", "/pets", "--method", "GET", "--robot"],
|
|
);
|
|
result.failure();
|
|
}
|
|
|
|
/// Doctor --fix should repair a missing index by rebuilding from raw.json.
|
|
#[test]
|
|
fn test_doctor_fix_repairs_missing_index() {
|
|
let env = helpers::TestEnv::new();
|
|
setup_with_cached_spec(&env);
|
|
|
|
let alias_dir = env.cache_dir.join("petstore");
|
|
let index_path = alias_dir.join("index.json");
|
|
|
|
// Remove index.json
|
|
fs::remove_file(&index_path).expect("failed to remove index.json");
|
|
|
|
// Doctor --fix should repair
|
|
let result = helpers::run_cmd(&env, &["doctor", "--fix", "--robot"]);
|
|
result.success();
|
|
|
|
// After fix, list should work again
|
|
let result = helpers::run_cmd(&env, &["list", "petstore", "--robot"]);
|
|
result.success();
|
|
}
|
|
|
|
/// Doctor --fix should repair a corrupted index (hash mismatch) by rebuilding.
|
|
#[test]
|
|
fn test_doctor_fix_repairs_corrupted_index() {
|
|
let env = helpers::TestEnv::new();
|
|
setup_with_cached_spec(&env);
|
|
|
|
let alias_dir = env.cache_dir.join("petstore");
|
|
let index_path = alias_dir.join("index.json");
|
|
|
|
// Corrupt the index
|
|
fs::write(&index_path, b"[]").expect("failed to corrupt index");
|
|
|
|
// Doctor --fix should detect and repair
|
|
let result = helpers::run_cmd(&env, &["doctor", "--fix", "--robot"]);
|
|
result.success();
|
|
|
|
// After fix, list should work
|
|
let result = helpers::run_cmd(&env, &["list", "petstore", "--robot"]);
|
|
result.success();
|
|
}
|
|
|
|
/// When meta.json has a generation mismatch with index.json, read should fail.
|
|
#[test]
|
|
fn test_generation_mismatch_detected() {
|
|
let env = helpers::TestEnv::new();
|
|
setup_with_cached_spec(&env);
|
|
|
|
let alias_dir = env.cache_dir.join("petstore");
|
|
let meta_path = alias_dir.join("meta.json");
|
|
|
|
// Tamper with generation in meta.json
|
|
let meta_bytes = fs::read(&meta_path).expect("failed to read meta");
|
|
let mut meta: serde_json::Value =
|
|
serde_json::from_slice(&meta_bytes).expect("failed to parse meta");
|
|
meta["generation"] = serde_json::json!(9999);
|
|
let tampered = serde_json::to_vec_pretty(&meta).expect("failed to serialize");
|
|
fs::write(&meta_path, &tampered).expect("failed to write tampered meta");
|
|
|
|
// Read should fail — generation mismatch
|
|
let result = helpers::run_cmd(&env, &["list", "petstore", "--robot"]);
|
|
result.failure();
|
|
}
|
|
|
|
/// Leftover .tmp files from a crash should not interfere with normal operation.
|
|
#[test]
|
|
fn test_leftover_tmp_files_harmless() {
|
|
let env = helpers::TestEnv::new();
|
|
setup_with_cached_spec(&env);
|
|
|
|
let alias_dir = env.cache_dir.join("petstore");
|
|
|
|
// Create leftover .tmp files (simulating interrupted write)
|
|
fs::write(alias_dir.join("raw.json.tmp"), b"partial data").expect("failed to create tmp file");
|
|
fs::write(alias_dir.join("index.json.tmp"), b"partial index")
|
|
.expect("failed to create tmp file");
|
|
|
|
// Normal operations should still work
|
|
let result = helpers::run_cmd(&env, &["list", "petstore", "--robot"]);
|
|
result.success();
|
|
}
|
|
|
|
/// A completely empty alias directory (no files at all) should be treated as not found.
|
|
#[test]
|
|
fn test_empty_alias_directory() {
|
|
let env = helpers::TestEnv::new();
|
|
|
|
// Create an empty directory that looks like an alias
|
|
let empty_alias_dir = env.cache_dir.join("ghost-alias");
|
|
fs::create_dir_all(&empty_alias_dir).expect("failed to create dir");
|
|
|
|
// Should fail — no meta.json
|
|
let result = helpers::run_cmd(&env, &["list", "ghost-alias", "--robot"]);
|
|
result.failure();
|
|
}
|