mirror of
https://github.com/openai/codex.git
synced 2026-09-17 12:23:33 +00:00
## Why PR #13783 moved the `codex.rs` unit tests into `codex_tests.rs`. This applies the same extraction pattern across the rest of `codex-rs/core` so the production modules stay focused on runtime code instead of large inline test blocks. Keeping the tests in sibling files also makes follow-up edits easier to review because product changes no longer have to share a file with hundreds or thousands of lines of test scaffolding. ## What changed - replaced each inline `mod tests { ... }` in `codex-rs/core/src/**` with a path-based module declaration - moved each extracted unit test module into a sibling `*_tests.rs` file, using `mod_tests.rs` for `mod.rs` modules - preserved the existing `cfg(...)` guards and module-local structure so the refactor remains structural rather than behavioral ## Testing - `cargo test -p codex-core --lib` (`1653 passed; 0 failed; 5 ignored`) - `just fix -p codex-core` - `cargo fmt --check` - `cargo shear`
150 lines
4.7 KiB
Rust
150 lines
4.7 KiB
Rust
use codex_protocol::custom_prompts::CustomPrompt;
|
|
use std::collections::HashSet;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use tokio::fs;
|
|
|
|
/// Return the default prompts directory: `$CODEX_HOME/prompts`.
|
|
/// If `CODEX_HOME` cannot be resolved, returns `None`.
|
|
pub fn default_prompts_dir() -> Option<PathBuf> {
|
|
crate::config::find_codex_home()
|
|
.ok()
|
|
.map(|home| home.join("prompts"))
|
|
}
|
|
|
|
/// Discover prompt files in the given directory, returning entries sorted by name.
|
|
/// Non-files are ignored. If the directory does not exist or cannot be read, returns empty.
|
|
pub async fn discover_prompts_in(dir: &Path) -> Vec<CustomPrompt> {
|
|
discover_prompts_in_excluding(dir, &HashSet::new()).await
|
|
}
|
|
|
|
/// Discover prompt files in the given directory, excluding any with names in `exclude`.
|
|
/// Returns entries sorted by name. Non-files are ignored. Missing/unreadable dir yields empty.
|
|
pub async fn discover_prompts_in_excluding(
|
|
dir: &Path,
|
|
exclude: &HashSet<String>,
|
|
) -> Vec<CustomPrompt> {
|
|
let mut out: Vec<CustomPrompt> = Vec::new();
|
|
let mut entries = match fs::read_dir(dir).await {
|
|
Ok(entries) => entries,
|
|
Err(_) => return out,
|
|
};
|
|
|
|
while let Ok(Some(entry)) = entries.next_entry().await {
|
|
let path = entry.path();
|
|
let is_file_like = fs::metadata(&path)
|
|
.await
|
|
.map(|m| m.is_file())
|
|
.unwrap_or(false);
|
|
if !is_file_like {
|
|
continue;
|
|
}
|
|
// Only include Markdown files with a .md extension.
|
|
let is_md = path
|
|
.extension()
|
|
.and_then(|s| s.to_str())
|
|
.map(|ext| ext.eq_ignore_ascii_case("md"))
|
|
.unwrap_or(false);
|
|
if !is_md {
|
|
continue;
|
|
}
|
|
let Some(name) = path
|
|
.file_stem()
|
|
.and_then(|s| s.to_str())
|
|
.map(str::to_string)
|
|
else {
|
|
continue;
|
|
};
|
|
if exclude.contains(&name) {
|
|
continue;
|
|
}
|
|
let content = match fs::read_to_string(&path).await {
|
|
Ok(s) => s,
|
|
Err(_) => continue,
|
|
};
|
|
let (description, argument_hint, body) = parse_frontmatter(&content);
|
|
out.push(CustomPrompt {
|
|
name,
|
|
path,
|
|
content: body,
|
|
description,
|
|
argument_hint,
|
|
});
|
|
}
|
|
out.sort_by(|a, b| a.name.cmp(&b.name));
|
|
out
|
|
}
|
|
|
|
/// Parse optional YAML-like frontmatter at the beginning of `content`.
|
|
/// Supported keys:
|
|
/// - `description`: short description shown in the slash popup
|
|
/// - `argument-hint` or `argument_hint`: brief hint string shown after the description
|
|
/// Returns (description, argument_hint, body_without_frontmatter).
|
|
fn parse_frontmatter(content: &str) -> (Option<String>, Option<String>, String) {
|
|
let mut segments = content.split_inclusive('\n');
|
|
let Some(first_segment) = segments.next() else {
|
|
return (None, None, String::new());
|
|
};
|
|
let first_line = first_segment.trim_end_matches(['\r', '\n']);
|
|
if first_line.trim() != "---" {
|
|
return (None, None, content.to_string());
|
|
}
|
|
|
|
let mut desc: Option<String> = None;
|
|
let mut hint: Option<String> = None;
|
|
let mut frontmatter_closed = false;
|
|
let mut consumed = first_segment.len();
|
|
|
|
for segment in segments {
|
|
let line = segment.trim_end_matches(['\r', '\n']);
|
|
let trimmed = line.trim();
|
|
|
|
if trimmed == "---" {
|
|
frontmatter_closed = true;
|
|
consumed += segment.len();
|
|
break;
|
|
}
|
|
|
|
if trimmed.is_empty() || trimmed.starts_with('#') {
|
|
consumed += segment.len();
|
|
continue;
|
|
}
|
|
|
|
if let Some((k, v)) = trimmed.split_once(':') {
|
|
let key = k.trim().to_ascii_lowercase();
|
|
let mut val = v.trim().to_string();
|
|
if val.len() >= 2 {
|
|
let bytes = val.as_bytes();
|
|
let first = bytes[0];
|
|
let last = bytes[bytes.len() - 1];
|
|
if (first == b'\"' && last == b'\"') || (first == b'\'' && last == b'\'') {
|
|
val = val[1..val.len().saturating_sub(1)].to_string();
|
|
}
|
|
}
|
|
match key.as_str() {
|
|
"description" => desc = Some(val),
|
|
"argument-hint" | "argument_hint" => hint = Some(val),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
consumed += segment.len();
|
|
}
|
|
|
|
if !frontmatter_closed {
|
|
// Unterminated frontmatter: treat input as-is.
|
|
return (None, None, content.to_string());
|
|
}
|
|
|
|
let body = if consumed >= content.len() {
|
|
String::new()
|
|
} else {
|
|
content[consumed..].to_string()
|
|
};
|
|
(desc, hint, body)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "custom_prompts_tests.rs"]
|
|
mod tests;
|