entrenar/cli/commands/research/
preregister.rs1use 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 let commitment = prereg.commit();
20 log(level, LogLevel::Verbose, &format!(" Commitment hash: {}...", &commitment.hash[..32]));
21
22 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 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 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 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 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 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 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 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 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}