pub mod builders;
pub(crate) mod language_nav;
pub mod source;
pub const SERVER_INSTRUCTIONS: &str =
include_str!(concat!(env!("OUT_DIR"), "/server_instructions.md"));
const KOTLIN_KNOWN_ISSUES: &str = "\n\n## Language Support — Known Issues\n\n\
### Kotlin (kotlin-lsp)\n\n\
kotlin-lsp (JetBrains) has a **single workspace session** limitation: only one \
kotlin-lsp process can serve a given project directory at a time. If another \
codescout instance or editor is already running kotlin-lsp for the same project, \
new instances will fail with:\n\n\
> \"Multiple editing sessions for one workspace are not supported yet\"\n\n\
codescout detects this and fails fast with a clear error. **Workaround:** close \
the other session first, or use a single codescout instance for Kotlin projects.";
pub fn build_server_instructions(project_status: Option<&ProjectStatus>) -> String {
let project_languages: Vec<Vec<String>> = match project_status {
Some(s) => {
let mut v: Vec<Vec<String>> = vec![s.languages.clone()];
if let Some(ws) = &s.workspace {
for p in ws {
v.push(p.languages.clone());
}
}
v
}
None => Vec::new(),
};
let nav_content = language_nav::render_symbol_navigation_block(&project_languages);
let mut instructions = SERVER_INSTRUCTIONS.replace(SYMBOL_NAV_TOKEN, &nav_content);
if let Some(status) = project_status {
instructions.push_str("\n\n## Project Status\n\n");
instructions.push_str(&format!(
"- **Project:** {} at `{}`\n",
status.name, status.path
));
if !status.languages.is_empty() {
instructions.push_str(&format!(
"- **Languages:** {}\n",
status.languages.join(", ")
));
}
if !status.memories.is_empty() {
instructions.push_str(&format!(
"- **Available shared memories:** {} — use `memory(action=\"read\", topic=...)` to read relevant ones as needed for your current task\n",
status.memories.join(", ")
));
} else {
instructions.push_str(
"- **Memories:** None yet — run `onboarding` to create project memories\n",
);
}
if status.has_index {
instructions
.push_str("- **Semantic index:** Built — `semantic_search` is ready to use\n");
} else {
instructions.push_str(
"- **Semantic index:** Not built — run `index(action='build')` to enable `semantic_search`\n",
);
}
if let Some(projects) = &status.workspace {
if !projects.is_empty() {
instructions.push_str("\n## Workspace Projects\n\n");
instructions.push_str("| Project | Root | Languages | Depends On |\n");
instructions.push_str("|---------|------|-----------|------------|\n");
for p in projects {
let langs = if p.languages.is_empty() {
"—".to_string()
} else {
p.languages.join(", ")
};
let deps = if p.depends_on.is_empty() {
"—".to_string()
} else {
p.depends_on.join(", ")
};
instructions.push_str(&format!(
"| {} | {} | {} | {} |\n",
p.id, p.root, langs, deps
));
}
instructions.push_str(
"\nUse `project: \"<id>\"` in `symbols` / `semantic_search` / `memory` to scope to a specific project.\n",
);
}
}
if status.languages.iter().any(|l| l == "kotlin") {
instructions.push_str(KOTLIN_KNOWN_ISSUES);
}
if let Some(prompt) = &status.system_prompt {
instructions.push_str("\n\n## Custom Instructions\n\n");
instructions.push_str(prompt);
instructions.push('\n');
}
}
instructions
}
#[derive(Debug)]
pub struct WorkspaceProjectSummary {
pub id: String,
pub root: String,
pub languages: Vec<String>,
pub depends_on: Vec<String>,
}
#[derive(Debug)]
pub struct ProjectStatus {
pub name: String,
pub path: String,
pub languages: Vec<String>,
pub memories: Vec<String>,
pub has_index: bool,
pub system_prompt: Option<String>,
pub workspace: Option<Vec<WorkspaceProjectSummary>>,
}
pub const INCLUDE_MARKER: &str = "{{include: memory-templates.md}}";
pub const SYMBOL_NAV_TOKEN: &str = "{{symbol_navigation_block}}";
pub(crate) const RAW_ONBOARDING_PROMPT: &str =
include_str!(concat!(env!("OUT_DIR"), "/onboarding_prompt.md"));
const RAW_WORKSPACE_ONBOARDING_PROMPT: &str = include_str!("workspace_onboarding_prompt.md");
const MEMORY_TEMPLATES: &str = include_str!("memory-templates.md");
pub fn load_prompt(name: &str) -> String {
let raw = match name {
"onboarding_prompt.md" => RAW_ONBOARDING_PROMPT,
"workspace_onboarding_prompt.md" => RAW_WORKSPACE_ONBOARDING_PROMPT,
other => panic!("unknown prompt: {other}"),
};
raw.replace(INCLUDE_MARKER, MEMORY_TEMPLATES)
}
pub struct OnboardingContext<'a> {
pub languages: &'a [String],
pub top_level: &'a [String],
pub key_files: &'a [String],
pub ci_files: &'a [String],
pub entry_points: &'a [String],
pub test_dirs: &'a [String],
pub index_ready: bool,
pub index_files: usize,
pub index_chunks: usize,
pub projects: &'a [crate::workspace::DiscoveredProject],
pub is_workspace: bool,
}
pub fn build_onboarding_prompt(ctx: &OnboardingContext) -> String {
let workspace_mode = ctx.is_workspace && ctx.projects.len() > 1;
let mut prompt = if workspace_mode {
load_prompt("workspace_onboarding_prompt.md")
} else {
load_prompt("onboarding_prompt.md")
};
prompt.push_str("\n\n---\n\n");
if !ctx.languages.is_empty() {
prompt.push_str(&format!(
"**Detected languages:** {}\n\n",
ctx.languages.join(", ")
));
}
if !ctx.top_level.is_empty() {
prompt.push_str(&format!(
"**Top-level structure:**\n```\n{}\n```\n\n",
ctx.top_level.join("\n")
));
}
if !ctx.entry_points.is_empty() {
prompt.push_str(&format!(
"**Entry points found:** {}\n\n",
ctx.entry_points.join(", ")
));
}
if !ctx.test_dirs.is_empty() {
prompt.push_str(&format!(
"**Test directories:** {}\n\n",
ctx.test_dirs.join(", ")
));
}
if !ctx.ci_files.is_empty() {
prompt.push_str(&format!(
"**CI config files:** {}\n\n",
ctx.ci_files.join(", ")
));
}
if !ctx.key_files.is_empty() {
prompt.push_str(&format!(
"**Key files to read during Phase 1:**\n{}\n\n",
ctx.key_files
.iter()
.map(|f| format!("- `{f}`"))
.collect::<Vec<_>>()
.join("\n")
));
}
if ctx.index_ready {
prompt.push_str(&format!(
"**Semantic index:** ready ({} files, {} chunks)\n\n",
ctx.index_files, ctx.index_chunks
));
} else {
prompt.push_str("**Semantic index:** not built\n\n");
}
if workspace_mode {
prompt.push_str(&format!(
"**Workspace mode:** {} projects detected\n\n",
ctx.projects.len()
));
prompt.push_str("**Discovered projects:**\n\n");
prompt.push_str("| Project | Root | Languages | Build |\n");
prompt.push_str("|---------|------|-----------|-------|\n");
for p in ctx.projects {
prompt.push_str(&format!(
"| {} | {} | {} | {} |\n",
p.id,
p.relative_root.display(),
p.languages.join(", "),
p.manifest.as_deref().unwrap_or("-"),
));
}
prompt.push('\n');
}
prompt
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn static_instructions_contain_key_sections() {
assert!(SERVER_INSTRUCTIONS.contains("## Tool Routing & Gotchas"));
assert!(SERVER_INSTRUCTIONS.contains("## Output System"));
assert!(SERVER_INSTRUCTIONS.contains("## Rules"));
}
#[test]
fn build_without_project_returns_substituted_static() {
let result = build_server_instructions(None);
assert!(!result.contains("{{symbol_navigation_block}}"));
assert!(result.contains("### Symbol Navigation Patterns"));
assert!(!result.contains("### Rust — Symbol Navigation"));
assert!(!result.contains("## Project Status"));
}
#[test]
fn iron_law_8_promotes_call_graph_before_references() {
let raw = SERVER_INSTRUCTIONS;
let idx = raw
.find("8. **CALL GRAPH BEFORE STRUCTURAL EDITS.**")
.expect("Iron Law 8 must be the call_graph promotion");
let body = &raw[idx..idx.saturating_add(500)];
let cg = body.find("call_graph").expect("call_graph must appear");
let refs = body.find("references").expect("references must appear");
assert!(cg < refs, "call_graph must be named before references");
}
#[test]
fn impact_analysis_section_contains_call_graph_with_full_arguments() {
let raw = SERVER_INSTRUCTIONS;
let section_start = raw.find("### Impact Analysis").expect("section must exist");
let next = raw[section_start..]
.find("\n### ")
.map(|i| section_start + i)
.unwrap_or(raw.len());
let section = &raw[section_start..next];
assert!(
section.contains("call_graph(symbol="),
"Impact Analysis must include a call_graph call with named symbol arg"
);
assert!(
section.contains("direction=\"callers\""),
"Impact Analysis must demonstrate direction=\"callers\""
);
assert!(
section.contains("max_depth=3"),
"Impact Analysis must demonstrate max_depth=3"
);
assert!(
section.contains("`references`"),
"Impact Analysis must reference the references tool"
);
}
#[test]
fn build_with_project_appends_status() {
let status = ProjectStatus {
name: "my-project".into(),
path: "/home/user/my-project".into(),
languages: vec!["rust".into(), "python".into()],
memories: vec!["architecture".into(), "conventions".into()],
has_index: true,
system_prompt: None,
workspace: None,
};
let result = build_server_instructions(Some(&status));
assert!(result.contains("## Project Status"));
assert!(result.contains("my-project"));
assert!(result.contains("rust, python"));
assert!(result.contains("architecture, conventions"));
assert!(result.contains("Semantic index:** Built"));
}
#[test]
fn build_with_no_memories_suggests_onboarding() {
let status = ProjectStatus {
name: "new-project".into(),
path: "/tmp/new".into(),
languages: vec![],
memories: vec![],
has_index: false,
system_prompt: None,
workspace: None,
};
let result = build_server_instructions(Some(&status));
assert!(result.contains("run `onboarding`"));
assert!(result.contains("run `index(action='build')`"));
}
#[test]
fn onboarding_prompt_contains_key_sections() {
let prompt = load_prompt("onboarding_prompt.md");
assert!(prompt.contains("## THE IRON LAW"));
assert!(prompt.contains("## Phase 0: Embedding Model Selection"));
assert!(prompt.contains("## Phase 1: Semantic Index Check"));
assert!(prompt.contains("## Phase 2: Explore the Code"));
assert!(prompt.contains("### project-scope: project-overview"));
assert!(prompt.contains("### project-scope: architecture"));
assert!(prompt.contains("## Coverage Verification"));
assert!(prompt.contains("### Refresh CLAUDE.md"));
}
#[test]
fn workspace_onboarding_prompt_contains_key_sections() {
let prompt = load_prompt("workspace_onboarding_prompt.md");
assert!(prompt.contains("# WORKSPACE MODE"));
assert!(prompt.contains("## Phase 1 — Workspace Survey"));
assert!(prompt.contains("## Phase 3 — Per-Project Deep Dives"));
assert!(prompt.contains("## Phase 4 — Coverage Verification"));
assert!(prompt.contains("## Phase 5 — Workspace Synthesis"));
assert!(prompt.contains("## Phase 6 — CLAUDE.md Refresh"));
}
#[test]
fn load_prompt_substitutes_include_marker() {
let single = load_prompt("onboarding_prompt.md");
let workspace = load_prompt("workspace_onboarding_prompt.md");
assert!(
!single.contains("{{include: memory-templates.md}}"),
"include marker should be substituted in single-project prompt"
);
assert!(
!workspace.contains("{{include: memory-templates.md}}"),
"include marker should be substituted in workspace prompt"
);
}
#[test]
fn build_onboarding_includes_languages() {
let result = build_onboarding_prompt(&OnboardingContext {
languages: &["rust".into(), "python".into()],
top_level: &["src/".into(), "tests/".into()],
key_files: &[],
ci_files: &[],
entry_points: &[],
test_dirs: &[],
index_ready: false,
index_files: 0,
index_chunks: 0,
projects: &[],
is_workspace: false,
});
assert!(result.contains("rust, python"));
assert!(result.contains("src/"));
}
#[test]
fn build_onboarding_handles_empty() {
let result = build_onboarding_prompt(&OnboardingContext {
languages: &[],
top_level: &[],
key_files: &[],
ci_files: &[],
entry_points: &[],
test_dirs: &[],
index_ready: false,
index_files: 0,
index_chunks: 0,
projects: &[],
is_workspace: false,
});
assert!(result.contains("## Rules"));
assert!(!result.contains("Detected languages"));
}
#[test]
fn build_onboarding_includes_gathered_context() {
let result = build_onboarding_prompt(&OnboardingContext {
languages: &["rust".into(), "python".into()],
top_level: &["src/".into(), "tests/".into()],
key_files: &["README.md".into(), "Cargo.toml".into(), "CLAUDE.md".into()],
ci_files: &[".github/workflows/ci.yml".into()],
entry_points: &["src/main.rs".into()],
test_dirs: &["tests".into()],
index_ready: false,
index_files: 0,
index_chunks: 0,
projects: &[],
is_workspace: false,
});
assert!(result.contains("Cargo.toml"));
assert!(result.contains("ci.yml"));
assert!(result.contains("src/main.rs"));
assert!(result.contains("Detected languages"));
}
#[test]
fn build_with_system_prompt_appends_custom_section() {
let status = ProjectStatus {
name: "my-project".into(),
path: "/tmp/my-project".into(),
languages: vec![],
memories: vec![],
has_index: false,
system_prompt: Some("Always use pytest.".into()),
workspace: None,
};
let result = build_server_instructions(Some(&status));
assert!(result.contains("## Custom Instructions"));
assert!(result.contains("Always use pytest."));
let status_pos = result.find("## Project Status").unwrap();
let custom_pos = result.find("## Custom Instructions").unwrap();
assert!(custom_pos > status_pos);
}
#[test]
fn build_without_system_prompt_has_no_custom_section() {
let status = ProjectStatus {
name: "my-project".into(),
path: "/tmp/my-project".into(),
languages: vec![],
memories: vec![],
has_index: false,
system_prompt: None,
workspace: None,
};
let result = build_server_instructions(Some(&status));
assert!(!result.contains("## Custom Instructions"));
}
#[test]
fn build_with_workspace_appends_project_table() {
let status = ProjectStatus {
name: "backend-kotlin".into(),
path: "/workspace/backend-kotlin".into(),
languages: vec!["kotlin".into()],
memories: vec![],
has_index: false,
system_prompt: None,
workspace: Some(vec![
WorkspaceProjectSummary {
id: "backend-kotlin".into(),
root: ".".into(),
languages: vec!["kotlin".into()],
depends_on: vec![],
},
WorkspaceProjectSummary {
id: "mcp-server".into(),
root: "mcp-server/".into(),
languages: vec!["typescript".into()],
depends_on: vec![],
},
WorkspaceProjectSummary {
id: "python-services".into(),
root: "python-services/".into(),
languages: vec!["python".into()],
depends_on: vec!["mcp-server".into()],
},
]),
};
let result = build_server_instructions(Some(&status));
assert!(result.contains("## Workspace Projects"));
assert!(result.contains("mcp-server"));
assert!(result.contains("python-services"));
assert!(result.contains("python-services/"));
assert!(result.contains("mcp-server"));
assert!(result.contains("project: \"<id>\""));
}
#[test]
fn build_with_single_project_no_workspace_table() {
let status = ProjectStatus {
name: "solo".into(),
path: "/solo".into(),
languages: vec!["rust".into()],
memories: vec![],
has_index: false,
system_prompt: None,
workspace: None,
};
let result = build_server_instructions(Some(&status));
assert!(!result.contains("## Workspace Projects"));
}
#[test]
fn build_onboarding_shows_index_ready() {
let result = build_onboarding_prompt(&OnboardingContext {
languages: &["rust".into()],
top_level: &[],
key_files: &[],
ci_files: &[],
entry_points: &[],
test_dirs: &[],
index_ready: true,
index_files: 42,
index_chunks: 350,
projects: &[],
is_workspace: false,
});
assert!(result.contains("Semantic index:** ready (42 files, 350 chunks)"));
}
#[test]
fn build_onboarding_shows_index_not_built() {
let result = build_onboarding_prompt(&OnboardingContext {
languages: &["rust".into()],
top_level: &[],
key_files: &[],
ci_files: &[],
entry_points: &[],
test_dirs: &[],
index_ready: false,
index_files: 0,
index_chunks: 0,
projects: &[],
is_workspace: false,
});
assert!(result.contains("Semantic index:** not built"));
}
#[test]
fn onboarding_prompt_includes_workspace_projects() {
use std::path::PathBuf;
let projects = vec![
crate::workspace::DiscoveredProject {
id: "api".to_string(),
relative_root: PathBuf::from("api"),
languages: vec!["rust".to_string()],
manifest: Some("Cargo.toml".to_string()),
},
crate::workspace::DiscoveredProject {
id: "frontend".to_string(),
relative_root: PathBuf::from("frontend"),
languages: vec!["typescript".to_string()],
manifest: Some("package.json".to_string()),
},
];
let ctx = OnboardingContext {
languages: &["rust".to_string(), "typescript".to_string()],
top_level: &["api/".to_string(), "frontend/".to_string()],
key_files: &[],
ci_files: &[],
entry_points: &["api/src/main.rs".to_string()],
test_dirs: &[],
index_ready: false,
index_files: 0,
index_chunks: 0,
projects: &projects,
is_workspace: true,
};
let prompt = build_onboarding_prompt(&ctx);
assert!(prompt.contains("Workspace"));
assert!(prompt.contains("Workspace Survey"));
assert!(prompt.contains("api"));
assert!(prompt.contains("frontend"));
}
#[test]
fn build_with_kotlin_project_includes_kotlin_warnings() {
let status = ProjectStatus {
name: "test".into(),
path: "/tmp/test".into(),
languages: vec!["kotlin".into(), "java".into()],
memories: vec![],
has_index: false,
system_prompt: None,
workspace: None,
};
let result = build_server_instructions(Some(&status));
assert!(
result.contains("kotlin-lsp"),
"Kotlin project must include Kotlin known issues"
);
}
#[test]
fn build_without_kotlin_excludes_kotlin_warnings() {
let status = ProjectStatus {
name: "test".into(),
path: "/tmp/test".into(),
languages: vec!["rust".into()],
memories: vec![],
has_index: false,
system_prompt: None,
workspace: None,
};
let result = build_server_instructions(Some(&status));
assert!(
!result.contains("kotlin-lsp"),
"Non-Kotlin project must not include Kotlin known issues"
);
}
#[test]
fn memory_templates_have_all_project_scope_sections() {
let templates = include_str!("memory-templates.md");
for topic in [
"project-overview",
"architecture",
"conventions",
"development-commands",
"domain-glossary",
"gotchas",
] {
let heading = format!("### project-scope: {topic}");
assert!(
templates.contains(&heading),
"memory-templates.md missing heading: {heading}"
);
}
}
#[test]
fn memory_templates_define_empty_stub() {
let templates = include_str!("memory-templates.md");
assert!(
templates.contains("EMPTY_STUB:"),
"memory-templates.md must define the canonical empty stub"
);
}
#[test]
fn memory_templates_have_all_workspace_scope_sections() {
let templates = include_str!("memory-templates.md");
for topic in [
"architecture",
"conventions",
"development-commands",
"domain-glossary",
"gotchas",
"system-prompt",
] {
let heading = format!("### workspace-scope: {topic}");
assert!(
templates.contains(&heading),
"memory-templates.md missing heading: {heading}"
);
}
}
#[test]
fn workspace_architecture_template_has_required_subsections() {
let templates = include_str!("memory-templates.md");
for sub in [
"Project Map",
"Cross-Project Dependencies",
"Shared Infrastructure",
"Top-Level Code Map",
"Generic Navigation",
] {
assert!(
templates.contains(&format!("- `## {sub}`")),
"workspace architecture template missing required subsection: {sub}"
);
}
}
#[test]
fn workspace_prompt_has_six_phases() {
let workspace = load_prompt("workspace_onboarding_prompt.md");
for phase in [
"## Phase 1 — Workspace Survey",
"## Phase 2 — Stale-Project Cleanup",
"## Phase 3 — Per-Project Deep Dives",
"## Phase 4 — Coverage Verification",
"## Phase 5 — Workspace Synthesis",
"## Phase 6 — CLAUDE.md Refresh",
] {
assert!(
workspace.contains(phase),
"workspace prompt missing phase: {phase}"
);
}
}
#[test]
fn workspace_prompt_requires_six_memories_per_project() {
let workspace = load_prompt("workspace_onboarding_prompt.md");
assert!(
workspace.contains("6 memories"),
"workspace subagent prompt must require 6 memories per project"
);
for topic in [
"project-overview",
"architecture",
"conventions",
"development-commands",
"domain-glossary",
"gotchas",
] {
assert!(
workspace.contains(topic),
"workspace prompt missing topic name: {topic}"
);
}
}
#[test]
fn onboarding_prompt_uses_include_marker() {
let raw = RAW_ONBOARDING_PROMPT;
assert!(
raw.contains("{{include: memory-templates.md}}"),
"onboarding_prompt.md must contain the include marker"
);
let loaded = load_prompt("onboarding_prompt.md");
assert!(!loaded.contains("{{include:"));
assert!(loaded.contains("### project-scope: project-overview"));
}
#[test]
fn onboarding_prompt_phase_0_has_stable_heading_marker() {
let raw = RAW_ONBOARDING_PROMPT;
assert!(
raw.contains("STABLE-HEADING"),
"Phase 0 must carry a STABLE-HEADING comment to prevent cross-prompt drift"
);
}
#[test]
fn workspace_phase_0_reference_resolves() {
let single = load_prompt("onboarding_prompt.md");
let workspace = load_prompt("workspace_onboarding_prompt.md");
let referenced = "## Phase 0: Embedding Model Selection";
if workspace.contains(referenced) {
assert!(
single.contains(referenced),
"workspace prompt references heading missing from single-project prompt"
);
}
}
#[test]
fn server_instructions_template_has_symbol_nav_token() {
let raw = SERVER_INSTRUCTIONS;
assert_eq!(
raw.matches("{{symbol_navigation_block}}").count(),
1,
"server_instructions.md must contain exactly one symbol_navigation_block token"
);
}
#[test]
fn build_server_instructions_substitutes_symbol_nav_token() {
let result = build_server_instructions(None);
assert!(
!result.contains("{{symbol_navigation_block}}"),
"token must be substituted in build_server_instructions output"
);
assert!(result.contains("### Symbol Navigation Patterns"));
assert!(result.contains("### Generic Patterns (any language)"));
}
#[test]
fn build_server_instructions_renders_languages_from_status() {
let status = ProjectStatus {
name: "x".into(),
path: "/tmp/x".into(),
languages: vec!["rust".into()],
memories: vec![],
has_index: false,
system_prompt: None,
workspace: None,
};
let result = build_server_instructions(Some(&status));
assert!(result.contains("### Rust — Symbol Navigation"));
}
#[test]
fn rendered_server_instructions_contains_no_deprecated_tool_names() {
let status = ProjectStatus {
name: "x".into(),
path: "/tmp/x".into(),
languages: vec![
"rust".into(),
"python".into(),
"typescript".into(),
"kotlin".into(),
"go".into(),
],
memories: vec![],
has_index: false,
system_prompt: None,
workspace: None,
};
let rendered = build_server_instructions(Some(&status));
for dead in [
"find_symbol",
"list_symbols",
"replace_symbol",
"insert_code",
"rename_symbol",
"search_pattern",
] {
assert!(
!rendered.contains(dead),
"rendered server instructions contains deprecated tool name: {dead}"
);
}
}
#[test]
fn build_server_instructions_has_no_duplicate_symbol_nav_heading() {
let result = build_server_instructions(None);
let count = result.matches("### Symbol Navigation Patterns").count();
assert_eq!(count, 1,
"### Symbol Navigation Patterns appears {count} times — LEAD_IN must not duplicate the section heading");
}
fn fixture_path(name: &str) -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/prompt_surfaces")
.join(name)
}
fn check_or_update_snapshot(name: &str, current: &str) {
let path = fixture_path(name);
if std::env::var("UPDATE_PROMPT_SNAPSHOTS").is_ok() {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("create fixture dir");
}
std::fs::write(&path, current).expect("write fixture");
eprintln!("updated snapshot: {}", path.display());
return;
}
let expected = std::fs::read_to_string(&path).unwrap_or_else(|e| {
panic!(
"missing snapshot `{}`: {e}\n\
Regenerate with: UPDATE_PROMPT_SNAPSHOTS=1 cargo test --lib prompt_surfaces",
path.display()
)
});
if expected != current {
panic!(
"prompt surface drift in `{name}`\n \
expected: {} bytes\n \
actual: {} bytes\n\n\
If intentional, regenerate with:\n\
\x20 UPDATE_PROMPT_SNAPSHOTS=1 cargo test --lib prompt_surfaces\n\n\
Otherwise this is a regression — I-01 (and any later prompt-template\n\
refactor) must preserve rendered content byte-for-byte.",
expected.len(),
current.len()
);
}
}
#[test]
fn prompt_surfaces_server_instructions_snapshot() {
check_or_update_snapshot("server_instructions.md", SERVER_INSTRUCTIONS);
}
#[test]
fn prompt_surfaces_onboarding_snapshot() {
check_or_update_snapshot("onboarding_prompt.md", RAW_ONBOARDING_PROMPT);
}
#[test]
fn prompt_surfaces_system_prompt_draft_empty_snapshot() {
let draft = crate::prompts::builders::build_system_prompt_draft(&[], &[], None, None, &[]);
check_or_update_snapshot("build_system_prompt_draft_empty.md", &draft);
}
}