Skip to main content

entrenar/cli/commands/research/
preregister.rs

1//! Research preregister subcommand
2
3use crate::cli::logging::log;
4use crate::cli::LogLevel;
5use crate::config::PreregisterArgs;
6use crate::research::{PreRegistration, SignedPreRegistration, TimestampProof};
7
8pub fn run_research_preregister(args: PreregisterArgs, level: LogLevel) -> Result<(), String> {
9    log(level, LogLevel::Normal, &format!("Creating pre-registration: {}", args.title));
10
11    let mut prereg =
12        PreRegistration::new(&args.title, &args.hypothesis, &args.methodology, &args.analysis_plan);
13
14    if let Some(notes) = &args.notes {
15        prereg = prereg.with_notes(notes);
16    }
17
18    // Create commitment
19    let commitment = prereg.commit();
20    log(level, LogLevel::Verbose, &format!("  Commitment hash: {}...", &commitment.hash[..32]));
21
22    // Sign if key provided
23    let output = if let Some(key_path) = &args.sign_key {
24        use ed25519_dalek::SigningKey;
25
26        let key_bytes =
27            std::fs::read(key_path).map_err(|e| format!("Failed to read signing key: {e}"))?;
28
29        if key_bytes.len() != 32 {
30            return Err("Signing key must be exactly 32 bytes".to_string());
31        }
32
33        let mut key_array = [0u8; 32];
34        key_array.copy_from_slice(&key_bytes);
35        let signing_key = SigningKey::from_bytes(&key_array);
36
37        let mut signed = SignedPreRegistration::sign(&prereg, &signing_key);
38
39        // Add git timestamp if requested
40        if args.git_timestamp {
41            let output = std::process::Command::new("git")
42                .args(["rev-parse", "HEAD"])
43                .output()
44                .map_err(|e| format!("Failed to get git commit: {e}"))?;
45
46            let commit_hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
47            signed = signed.with_timestamp_proof(TimestampProof::git(&commit_hash));
48            log(level, LogLevel::Verbose, &format!("  Git timestamp: {commit_hash}"));
49        }
50
51        serde_yaml::to_string(&signed).map_err(|e| format!("Serialization error: {e}"))?
52    } else {
53        // Output commitment without signature
54        serde_yaml::to_string(&commitment).map_err(|e| format!("Serialization error: {e}"))?
55    };
56
57    std::fs::write(&args.output, &output).map_err(|e| format!("Failed to write file: {e}"))?;
58
59    log(level, LogLevel::Normal, &format!("Pre-registration saved to: {}", args.output.display()));
60
61    Ok(())
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use tempfile::NamedTempFile;
68
69    fn make_test_args(output: std::path::PathBuf) -> PreregisterArgs {
70        PreregisterArgs {
71            title: "Test Title".to_string(),
72            hypothesis: "Test Hypothesis".to_string(),
73            methodology: "Test Methodology".to_string(),
74            analysis_plan: "Test Analysis Plan".to_string(),
75            notes: None,
76            output,
77            sign_key: None,
78            git_timestamp: false,
79        }
80    }
81
82    #[test]
83    fn test_preregister_basic() {
84        let output_file = NamedTempFile::new().expect("temp file creation should succeed");
85        let args = make_test_args(output_file.path().to_path_buf());
86        let result = run_research_preregister(args, LogLevel::Quiet);
87        assert!(result.is_ok());
88        // Verify output was created
89        let content =
90            std::fs::read_to_string(output_file.path()).expect("file read should succeed");
91        assert!(content.contains("hash:"));
92    }
93
94    #[test]
95    fn test_preregister_with_notes() {
96        let output_file = NamedTempFile::new().expect("temp file creation should succeed");
97        let mut args = make_test_args(output_file.path().to_path_buf());
98        args.notes = Some("Additional notes".to_string());
99        let result = run_research_preregister(args, LogLevel::Quiet);
100        assert!(result.is_ok());
101    }
102
103    #[test]
104    fn test_preregister_with_signing() {
105        let output_file = NamedTempFile::new().expect("temp file creation should succeed");
106        let key_file = NamedTempFile::new().expect("temp file creation should succeed");
107
108        // Write a valid 32-byte key
109        std::fs::write(key_file.path(), [1u8; 32]).expect("file write should succeed");
110
111        let mut args = make_test_args(output_file.path().to_path_buf());
112        args.sign_key = Some(key_file.path().to_path_buf());
113
114        let result = run_research_preregister(args, LogLevel::Quiet);
115        assert!(result.is_ok());
116
117        // Verify signed output
118        let content =
119            std::fs::read_to_string(output_file.path()).expect("file read should succeed");
120        assert!(content.contains("signature:"));
121        assert!(content.contains("public_key:"));
122    }
123
124    #[test]
125    fn test_preregister_invalid_key_size() {
126        let output_file = NamedTempFile::new().expect("temp file creation should succeed");
127        let key_file = NamedTempFile::new().expect("temp file creation should succeed");
128
129        // Write invalid key size (not 32 bytes)
130        std::fs::write(key_file.path(), [1u8; 16]).expect("file write should succeed");
131
132        let mut args = make_test_args(output_file.path().to_path_buf());
133        args.sign_key = Some(key_file.path().to_path_buf());
134
135        let result = run_research_preregister(args, LogLevel::Quiet);
136        assert!(result.is_err());
137        assert!(result.unwrap_err().contains("32 bytes"));
138    }
139
140    #[test]
141    fn test_preregister_missing_key_file() {
142        let output_file = NamedTempFile::new().expect("temp file creation should succeed");
143        let mut args = make_test_args(output_file.path().to_path_buf());
144        args.sign_key = Some(std::path::PathBuf::from("/nonexistent/key/file"));
145
146        let result = run_research_preregister(args, LogLevel::Quiet);
147        assert!(result.is_err());
148        assert!(result.unwrap_err().contains("Failed to read signing key"));
149    }
150
151    #[test]
152    fn test_preregister_invalid_output_path() {
153        let args = make_test_args(std::path::PathBuf::from("/nonexistent/dir/output.yaml"));
154        let result = run_research_preregister(args, LogLevel::Quiet);
155        assert!(result.is_err());
156        assert!(result.unwrap_err().contains("Failed to write file"));
157    }
158
159    #[test]
160    fn test_preregister_with_git_timestamp() {
161        // Skip if not in a git repo
162        let in_git = std::process::Command::new("git")
163            .args(["rev-parse", "HEAD"])
164            .output()
165            .map(|o| o.status.success())
166            .unwrap_or(false);
167
168        if !in_git {
169            return;
170        }
171
172        let output_file = NamedTempFile::new().expect("temp file creation should succeed");
173        let key_file = NamedTempFile::new().expect("temp file creation should succeed");
174        std::fs::write(key_file.path(), [2u8; 32]).expect("file write should succeed");
175
176        let mut args = make_test_args(output_file.path().to_path_buf());
177        args.sign_key = Some(key_file.path().to_path_buf());
178        args.git_timestamp = true;
179
180        let result = run_research_preregister(args, LogLevel::Quiet);
181        assert!(result.is_ok());
182
183        let content =
184            std::fs::read_to_string(output_file.path()).expect("file read should succeed");
185        assert!(content.contains("timestamp_proof:") || content.contains("GitCommit"));
186    }
187
188    #[test]
189    fn test_preregister_verbose_logging() {
190        let output_file = NamedTempFile::new().expect("temp file creation should succeed");
191        let args = make_test_args(output_file.path().to_path_buf());
192        // Just ensure it doesn't panic with verbose logging
193        let result = run_research_preregister(args, LogLevel::Verbose);
194        assert!(result.is_ok());
195    }
196
197    #[test]
198    fn test_preregister_with_signing_verbose() {
199        let output_file = NamedTempFile::new().expect("temp file creation should succeed");
200        let key_file = NamedTempFile::new().expect("temp file creation should succeed");
201        std::fs::write(key_file.path(), [3u8; 32]).expect("file write should succeed");
202
203        let mut args = make_test_args(output_file.path().to_path_buf());
204        args.sign_key = Some(key_file.path().to_path_buf());
205
206        let result = run_research_preregister(args, LogLevel::Verbose);
207        assert!(result.is_ok());
208    }
209}