#![allow(dead_code)]
use crate::models::SystemPrompt;
use crate::project_context::{ProjectContext, load_project_context_with_parents};
use crate::tui::app::AppMode;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, Default)]
pub struct PromptSessionContext<'a> {
pub user_memory_block: Option<&'a str>,
pub goal_objective: Option<&'a str>,
}
pub const HANDOFF_RELATIVE_PATH: &str = ".deepseek/handoff.md";
const INSTRUCTIONS_FILE_MAX_BYTES: usize = 100 * 1024;
fn render_instructions_block(paths: &[PathBuf]) -> Option<String> {
let mut sections: Vec<String> = Vec::new();
for path in paths {
match std::fs::read_to_string(path) {
Ok(raw) => {
let trimmed = raw.trim();
if trimmed.is_empty() {
continue;
}
let body = if trimmed.len() > INSTRUCTIONS_FILE_MAX_BYTES {
let head_end = (0..=INSTRUCTIONS_FILE_MAX_BYTES)
.rev()
.find(|&i| trimmed.is_char_boundary(i))
.unwrap_or(0);
format!("{}\n[…elided]", &trimmed[..head_end])
} else {
trimmed.to_string()
};
sections.push(format!(
"<instructions source=\"{}\">\n{}\n</instructions>",
path.display(),
body
));
}
Err(err) => {
tracing::warn!(
target: "instructions",
?err,
?path,
"skipping unreadable instructions file"
);
}
}
}
if sections.is_empty() {
None
} else {
Some(sections.join("\n\n"))
}
}
fn load_handoff_block(workspace: &Path) -> Option<String> {
let path = workspace.join(HANDOFF_RELATIVE_PATH);
let raw = std::fs::read_to_string(&path).ok()?;
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
Some(format!(
"## Previous Session Handoff\n\nThe previous session in this workspace left a handoff at `{}`. Consider it the first artifact to read on this turn — open blockers, in-flight changes, and recent decisions live there. Update or rewrite it before exiting if state changes materially.\n\n{}",
HANDOFF_RELATIVE_PATH, trimmed
))
}
pub const BASE_PROMPT: &str = include_str!("prompts/base.md");
pub const CALM_PERSONALITY: &str = include_str!("prompts/personalities/calm.md");
pub const PLAYFUL_PERSONALITY: &str = include_str!("prompts/personalities/playful.md");
pub const AGENT_MODE: &str = include_str!("prompts/modes/agent.md");
pub const PLAN_MODE: &str = include_str!("prompts/modes/plan.md");
pub const YOLO_MODE: &str = include_str!("prompts/modes/yolo.md");
pub const AUTO_APPROVAL: &str = include_str!("prompts/approvals/auto.md");
pub const SUGGEST_APPROVAL: &str = include_str!("prompts/approvals/suggest.md");
pub const NEVER_APPROVAL: &str = include_str!("prompts/approvals/never.md");
pub const COMPACT_TEMPLATE: &str = include_str!("prompts/compact.md");
pub const AGENT_PROMPT: &str = include_str!("prompts/agent.txt");
pub const YOLO_PROMPT: &str = include_str!("prompts/yolo.txt");
pub const PLAN_PROMPT: &str = include_str!("prompts/plan.txt");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Personality {
Calm,
Playful,
}
impl Personality {
#[must_use]
pub fn from_settings(calm_mode: bool) -> Self {
if calm_mode {
Self::Calm
} else {
Self::Calm
}
}
fn prompt(self) -> &'static str {
match self {
Self::Calm => CALM_PERSONALITY,
Self::Playful => PLAYFUL_PERSONALITY,
}
}
}
fn mode_prompt(mode: AppMode) -> &'static str {
match mode {
AppMode::Agent => AGENT_MODE,
AppMode::Yolo => YOLO_MODE,
AppMode::Plan => PLAN_MODE,
}
}
fn approval_prompt(mode: AppMode) -> &'static str {
match mode {
AppMode::Agent => SUGGEST_APPROVAL,
AppMode::Yolo => AUTO_APPROVAL,
AppMode::Plan => NEVER_APPROVAL,
}
}
pub fn compose_prompt(mode: AppMode, personality: Personality) -> String {
let parts: [&str; 4] = [
BASE_PROMPT.trim(),
personality.prompt().trim(),
mode_prompt(mode).trim(),
approval_prompt(mode).trim(),
];
let mut out =
String::with_capacity(parts.iter().map(|p| p.len()).sum::<usize>() + (parts.len() - 1) * 2);
for (i, part) in parts.iter().enumerate() {
if i > 0 {
out.push('\n');
out.push('\n');
}
out.push_str(part);
}
out
}
fn compose_mode_prompt(mode: AppMode) -> String {
compose_prompt(mode, Personality::Calm)
}
pub fn system_prompt_for_mode(mode: AppMode) -> SystemPrompt {
SystemPrompt::Text(compose_mode_prompt(mode))
}
pub fn system_prompt_for_mode_with_personality(
mode: AppMode,
personality: Personality,
) -> SystemPrompt {
SystemPrompt::Text(compose_prompt(mode, personality))
}
pub fn system_prompt_for_mode_with_context(
mode: AppMode,
workspace: &Path,
working_set_summary: Option<&str>,
) -> SystemPrompt {
system_prompt_for_mode_with_context_and_skills(
mode,
workspace,
working_set_summary,
None,
None,
None,
)
}
pub fn system_prompt_for_mode_with_context_and_skills(
mode: AppMode,
workspace: &Path,
working_set_summary: Option<&str>,
skills_dir: Option<&Path>,
instructions: Option<&[PathBuf]>,
user_memory_block: Option<&str>,
) -> SystemPrompt {
system_prompt_for_mode_with_context_skills_and_session(
mode,
workspace,
working_set_summary,
skills_dir,
instructions,
PromptSessionContext {
user_memory_block,
goal_objective: None,
},
)
}
pub fn system_prompt_for_mode_with_context_skills_and_session(
mode: AppMode,
workspace: &Path,
_working_set_summary: Option<&str>,
skills_dir: Option<&Path>,
instructions: Option<&[PathBuf]>,
session_context: PromptSessionContext<'_>,
) -> SystemPrompt {
let mode_prompt = compose_mode_prompt(mode);
let project_context = load_project_context_with_parents(workspace);
let mut full_prompt = if let Some(project_block) = project_context.as_system_block() {
format!("{}\n\n{}", mode_prompt, project_block)
} else {
let summary = crate::utils::summarize_project(workspace);
let tree = crate::utils::project_tree(workspace, 2); format!(
"{}\n\n### Project Structure (Automatic Map)\n**Summary:** {}\n\n**Tree:**\n```\n{}\n```",
mode_prompt, summary, tree
)
};
if let Some(paths) = instructions
&& let Some(block) = render_instructions_block(paths)
{
full_prompt = format!("{full_prompt}\n\n{block}");
}
if let Some(memory_block) = session_context.user_memory_block
&& !memory_block.trim().is_empty()
{
full_prompt = format!("{full_prompt}\n\n{memory_block}");
}
if let Some(goal_objective) = session_context.goal_objective
&& !goal_objective.trim().is_empty()
{
full_prompt = format!(
"{full_prompt}\n\n## Current Session Goal\n\n<session_goal>\n{}\n</session_goal>",
goal_objective.trim()
);
}
let skills_block = crate::skills::render_available_skills_context_for_workspace(workspace)
.or_else(|| skills_dir.and_then(crate::skills::render_available_skills_context));
if let Some(block) = skills_block {
full_prompt = format!("{full_prompt}\n\n{block}");
}
if matches!(mode, AppMode::Agent | AppMode::Yolo) {
full_prompt.push_str(
"\n\n## Context Management\n\n\
When the conversation gets long (you'll see a context usage indicator), you can:\n\
1. Use `/compact` to summarize earlier context and free up space\n\
2. The system will preserve important information (files you're working on, recent messages, tool results)\n\
3. After compaction, you'll see a summary of what was discussed and can continue seamlessly\n\n\
If you notice context is getting long (>80%), proactively suggest using `/compact` to the user.\n\n\
### Prompt-cache awareness\n\n\
DeepSeek caches the longest *byte-stable prefix* of every request and charges roughly 100× less for cache-hit tokens than miss tokens. The system prompt above is layered most-static-first specifically so the prefix stays stable turn-over-turn. To keep cache hits high:\n\
- **Working set location:** the current repo working set is injected into the latest user message inside a `<turn_meta>` block. Treat it as high-priority turn metadata, not as a stable system-prompt section.\n\
- **Append, don't reorder.** New context goes at the end (latest user / tool messages). Reshuffling earlier messages or rewriting their content invalidates the cache for everything after the change.\n\
- **Don't paraphrase quoted content.** If you've already read a file, refer to it by path or line range instead of re-quoting it with different formatting.\n\
- **Use `/compact` as a hard reset, not a tweak.** Compaction is meant for when the cache is already losing — it intentionally rewrites the prefix to a shorter summary. Don't trigger it for small wins.\n\
- **Read once, refer back.** Re-reading the same file produces a different tool-result envelope than the prior read; it's cheaper to scroll back than to re-fetch.\n\
- **Footer chip:** the `cache hit %` chip turns red below 40% and yellow below 80%. If it's been red for several turns, that's a signal to consolidate."
);
}
full_prompt.push_str("\n\n");
full_prompt.push_str(COMPACT_TEMPLATE);
if let Some(handoff_block) = load_handoff_block(workspace) {
full_prompt = format!("{full_prompt}\n\n{handoff_block}");
}
SystemPrompt::Text(full_prompt)
}
pub fn build_system_prompt(base: &str, project_context: Option<&ProjectContext>) -> SystemPrompt {
let full_prompt =
match project_context.and_then(super::project_context::ProjectContext::as_system_block) {
Some(project_block) => format!("{}\n\n{}", base.trim(), project_block),
None => base.trim().to_string(),
};
SystemPrompt::Text(full_prompt)
}
pub fn base_system_prompt() -> SystemPrompt {
SystemPrompt::Text(BASE_PROMPT.trim().to_string())
}
pub fn normal_system_prompt() -> SystemPrompt {
system_prompt_for_mode(AppMode::Agent)
}
pub fn agent_system_prompt() -> SystemPrompt {
system_prompt_for_mode(AppMode::Agent)
}
pub fn yolo_system_prompt() -> SystemPrompt {
system_prompt_for_mode(AppMode::Yolo)
}
pub fn plan_system_prompt() -> SystemPrompt {
system_prompt_for_mode(AppMode::Plan)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
const HANDOFF_BLOCK_MARKER: &str = "left a handoff at `.deepseek/handoff.md`";
#[test]
fn handoff_artifact_is_prepended_to_system_prompt_when_present() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let handoff_dir = workspace.join(".deepseek");
std::fs::create_dir_all(&handoff_dir).unwrap();
std::fs::write(
handoff_dir.join("handoff.md"),
"# Session handoff — prior\n\n## Active task\nFinish #32.\n\n## Open blockers\n- [ ] write the basic version\n",
)
.unwrap();
let prompt = match system_prompt_for_mode_with_context(AppMode::Agent, workspace, None) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert!(prompt.contains(HANDOFF_BLOCK_MARKER));
assert!(prompt.contains("Finish #32."));
assert!(prompt.contains("write the basic version"));
}
#[test]
fn missing_handoff_does_not_inject_block() {
let tmp = tempdir().expect("tempdir");
let prompt = match system_prompt_for_mode_with_context(AppMode::Agent, tmp.path(), None) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert!(!prompt.contains(HANDOFF_BLOCK_MARKER));
}
#[test]
fn empty_handoff_file_does_not_inject_block() {
let tmp = tempdir().expect("tempdir");
let dir = tmp.path().join(".deepseek");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("handoff.md"), " \n\n ").unwrap();
let prompt = match system_prompt_for_mode_with_context(AppMode::Agent, tmp.path(), None) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert!(!prompt.contains(HANDOFF_BLOCK_MARKER));
}
#[test]
fn compose_prompt_includes_all_layers() {
let prompt = compose_prompt(AppMode::Agent, Personality::Calm);
assert!(prompt.contains("You are DeepSeek TUI"));
assert!(prompt.contains("Personality: Calm"));
assert!(prompt.contains("Mode: Agent"));
assert!(prompt.contains("Approval Policy: Suggest"));
}
#[test]
fn compose_prompt_deterministic_order() {
let prompt = compose_prompt(AppMode::Yolo, Personality::Calm);
let base_pos = prompt.find("You are DeepSeek TUI").unwrap();
let personality_pos = prompt.find("Personality: Calm").unwrap();
let mode_pos = prompt.find("Mode: YOLO").unwrap();
let approval_pos = prompt.find("Approval Policy: Auto").unwrap();
assert!(base_pos < personality_pos);
assert!(personality_pos < mode_pos);
assert!(mode_pos < approval_pos);
}
#[test]
fn each_mode_gets_correct_approval() {
assert!(
compose_prompt(AppMode::Agent, Personality::Calm).contains("Approval Policy: Suggest")
);
assert!(compose_prompt(AppMode::Yolo, Personality::Calm).contains("Approval Policy: Auto"));
assert!(
compose_prompt(AppMode::Plan, Personality::Calm).contains("Approval Policy: Never")
);
}
#[test]
fn personality_switches_correctly() {
let calm = compose_prompt(AppMode::Agent, Personality::Calm);
let playful = compose_prompt(AppMode::Agent, Personality::Playful);
assert!(calm.contains("Personality: Calm"));
assert!(playful.contains("Personality: Playful"));
assert!(!calm.contains("Personality: Playful"));
}
#[test]
fn compact_template_is_included_in_full_prompt() {
let tmp = tempdir().expect("tempdir");
let prompt = match system_prompt_for_mode_with_context(AppMode::Agent, tmp.path(), None) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert!(prompt.contains("## Compaction Handoff"));
assert!(prompt.contains("### Goal"));
assert!(prompt.contains("### Constraints"));
assert!(prompt.contains("### Progress"));
assert!(prompt.contains("#### Done"));
assert!(prompt.contains("#### In Progress"));
assert!(prompt.contains("#### Blocked"));
assert!(prompt.contains("### Key Decisions"));
assert!(prompt.contains("### Next step"));
}
#[test]
fn session_goal_is_injected_above_handoff_tail() {
let tmp = tempdir().expect("tempdir");
let prompt = match system_prompt_for_mode_with_context_skills_and_session(
AppMode::Agent,
tmp.path(),
Some("## Repo Working Set\nsrc/lib.rs"),
None,
None,
PromptSessionContext {
user_memory_block: None,
goal_objective: Some("Fix transcript corruption"),
},
) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
let goal_pos = prompt.find("<session_goal>").expect("goal block");
let compact_pos = prompt.find("## Compaction Handoff").expect("compact block");
assert!(prompt.contains("Fix transcript corruption"));
assert!(goal_pos < compact_pos);
assert!(!prompt.contains("src/lib.rs"));
}
#[test]
fn empty_session_goal_is_not_injected() {
let tmp = tempdir().expect("tempdir");
let prompt = match system_prompt_for_mode_with_context_skills_and_session(
AppMode::Agent,
tmp.path(),
None,
None,
None,
PromptSessionContext {
user_memory_block: None,
goal_objective: Some(" "),
},
) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert!(!prompt.contains("<session_goal>"));
assert!(!prompt.contains("## Current Session Goal"));
}
#[test]
fn when_not_to_use_sections_present() {
let prompt = compose_prompt(AppMode::Agent, Personality::Calm);
assert!(prompt.contains("When NOT to use certain tools"));
assert!(prompt.contains("### `apply_patch`"));
assert!(prompt.contains("### `edit_file`"));
assert!(prompt.contains("### `exec_shell`"));
assert!(prompt.contains("### `agent_spawn`"));
assert!(prompt.contains("### `rlm`"));
}
#[test]
fn language_mirroring_section_present_in_all_modes() {
for mode in [AppMode::Agent, AppMode::Yolo, AppMode::Plan] {
let prompt = compose_prompt(mode, Personality::Calm);
assert!(
prompt.contains("## Language"),
"## Language section missing from mode {mode:?}"
);
assert!(
prompt.contains("reasoning_content"),
"## Language section in {mode:?} must mention `reasoning_content` — \
that field name is the structural anchor for the #588 commitment that \
internal reasoning, not just the visible reply, follows the user's language"
);
}
}
#[test]
fn rlm_specialty_tool_guidance_present() {
let prompt = compose_prompt(AppMode::Agent, Personality::Calm);
assert!(prompt.contains("RLM — When to Use It"));
let rlm_count = prompt.to_lowercase().matches("rlm").count();
assert!(
rlm_count >= 5,
"RLM guidance present: expected >= 5 mentions of 'rlm', got {rlm_count}"
);
}
#[test]
fn subagent_done_sentinel_section_present() {
let prompt = compose_prompt(AppMode::Agent, Personality::Calm);
assert!(prompt.contains("Sub-agent completion sentinel"));
assert!(prompt.contains("<deepseek:subagent.done>"));
assert!(prompt.contains("Integration protocol"));
}
#[test]
fn preamble_rhythm_section_present() {
let prompt = compose_prompt(AppMode::Agent, Personality::Calm);
assert!(prompt.contains("Preamble Rhythm"));
assert!(prompt.contains("I'll start by reading the module structure"));
}
#[test]
fn legacy_constants_still_available() {
assert!(!AGENT_PROMPT.is_empty());
assert!(!YOLO_PROMPT.is_empty());
assert!(!PLAN_PROMPT.is_empty());
}
use crate::test_support::assert_byte_identical;
#[test]
fn compose_prompt_is_byte_stable_across_calls() {
for mode in [AppMode::Agent, AppMode::Yolo, AppMode::Plan] {
for personality in [Personality::Calm, Personality::Playful] {
let a = compose_prompt(mode, personality);
let b = compose_prompt(mode, personality);
assert_byte_identical(
&format!("compose_prompt(mode={mode:?}, personality={personality:?})"),
&a,
&b,
);
}
}
}
#[test]
fn system_prompt_for_mode_with_context_is_byte_stable_for_unchanged_workspace() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
for mode in [AppMode::Agent, AppMode::Yolo, AppMode::Plan] {
let a = match system_prompt_for_mode_with_context(mode, workspace, None) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
let b = match system_prompt_for_mode_with_context(mode, workspace, None) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert_byte_identical(
&format!("system_prompt_for_mode_with_context(mode={mode:?}) on empty workspace"),
&a,
&b,
);
}
}
#[test]
fn system_prompt_ignores_working_set_summary_argument() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let summary = "## Repo Working Set\nWorkspace: /tmp/x\n";
let a = match system_prompt_for_mode_with_context(AppMode::Agent, workspace, Some(summary))
{
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
let b = match system_prompt_for_mode_with_context(AppMode::Agent, workspace, Some(summary))
{
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert_byte_identical(
"system_prompt_for_mode_with_context with constant working_set summary",
&a,
&b,
);
assert!(
!a.contains(summary),
"summary must not be embedded in system prompt"
);
}
#[test]
fn system_prompt_with_handoff_file_is_byte_stable_when_file_is_unchanged() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let handoff_dir = workspace.join(".deepseek");
std::fs::create_dir_all(&handoff_dir).unwrap();
std::fs::write(
handoff_dir.join("handoff.md"),
"# Session handoff\n\n## Active task\nFinish #280.\n\n## Open blockers\n- [ ] none\n",
)
.unwrap();
let a = match system_prompt_for_mode_with_context(AppMode::Agent, workspace, None) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
let b = match system_prompt_for_mode_with_context(AppMode::Agent, workspace, None) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert_byte_identical(
"system_prompt_for_mode_with_context with constant handoff file",
&a,
&b,
);
assert!(a.contains(HANDOFF_BLOCK_MARKER), "handoff must be embedded");
assert!(a.contains("Finish #280."), "handoff body must be present");
}
#[test]
fn handoff_appears_after_static_blocks_without_working_set() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let handoff_dir = workspace.join(".deepseek");
std::fs::create_dir_all(&handoff_dir).unwrap();
std::fs::write(handoff_dir.join("handoff.md"), "# handoff body\n").unwrap();
let summary = "## Repo Working Set\nWorkspace: /tmp/x\n";
let prompt =
match system_prompt_for_mode_with_context(AppMode::Agent, workspace, Some(summary)) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
let context_pos = prompt
.find("## Context Management")
.expect("Context Management section present in Agent mode");
let compact_pos = prompt
.find("## Compaction Handoff")
.expect("compaction handoff template present");
let handoff_pos = prompt
.find(HANDOFF_BLOCK_MARKER)
.expect("handoff block present when fixture file exists");
assert!(
!prompt.contains("## Repo Working Set"),
"working-set summary must stay out of the system prompt"
);
assert!(
context_pos < handoff_pos,
"## Context Management must precede the handoff block"
);
assert!(
compact_pos < handoff_pos,
"## Compaction Handoff must precede the handoff block"
);
}
#[test]
fn render_instructions_block_returns_none_for_empty_input() {
assert!(super::render_instructions_block(&[]).is_none());
}
#[test]
fn render_instructions_block_skips_missing_files_with_warning() {
let tmp = tempdir().expect("tempdir");
let real = tmp.path().join("real.md");
std::fs::write(&real, "real content here").unwrap();
let bogus = tmp.path().join("does-not-exist.md");
let block = super::render_instructions_block(&[bogus.clone(), real.clone()])
.expect("present file should produce a block");
assert!(block.contains("real content here"));
assert!(block.contains(&real.display().to_string()));
assert!(!block.contains(&bogus.display().to_string()));
}
#[test]
fn render_instructions_block_concatenates_in_declared_order() {
let tmp = tempdir().expect("tempdir");
let a = tmp.path().join("a.md");
let b = tmp.path().join("b.md");
std::fs::write(&a, "ALPHA_MARKER").unwrap();
std::fs::write(&b, "BRAVO_MARKER").unwrap();
let block = super::render_instructions_block(&[a, b]).expect("non-empty");
let alpha_pos = block.find("ALPHA_MARKER").expect("alpha rendered");
let bravo_pos = block.find("BRAVO_MARKER").expect("bravo rendered");
assert!(
alpha_pos < bravo_pos,
"instructions must concatenate in declared order"
);
}
#[test]
fn render_instructions_block_skips_empty_files() {
let tmp = tempdir().expect("tempdir");
let empty = tmp.path().join("empty.md");
let real = tmp.path().join("real.md");
std::fs::write(&empty, " \n \n").unwrap();
std::fs::write(&real, "real content").unwrap();
let block = super::render_instructions_block(&[empty, real]).expect("non-empty");
let count = block.matches("<instructions").count();
assert_eq!(count, 1, "only the non-empty file should produce a section");
}
#[test]
fn render_instructions_block_truncates_oversize_files() {
let tmp = tempdir().expect("tempdir");
let big = tmp.path().join("big.md");
std::fs::write(&big, "X".repeat(200 * 1024)).unwrap();
let block = super::render_instructions_block(&[big]).expect("non-empty");
assert!(block.contains("[…elided]"), "truncation marker missing");
assert!(
block.len() < 110 * 1024,
"block should be capped near 100 KiB"
);
}
#[test]
fn instructions_block_appears_in_system_prompt_when_configured() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let extra = workspace.join("extra-instructions.md");
std::fs::write(&extra, "EXTRA_INSTRUCTIONS_MARKER_BODY").unwrap();
let prompt = match super::system_prompt_for_mode_with_context_and_skills(
AppMode::Agent,
workspace,
None,
None,
Some(std::slice::from_ref(&extra)),
None,
) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert!(
prompt.contains("EXTRA_INSTRUCTIONS_MARKER_BODY"),
"configured instructions file body must appear in the prompt"
);
assert!(
prompt.contains(&extra.display().to_string()),
"instructions block must annotate its source path"
);
}
}