#![allow(dead_code)]
use crate::models::SystemPrompt;
use crate::project_context::{ProjectContext, load_project_context_with_parents};
use crate::tui::app::AppMode;
use crate::tui::approval::ApprovalMode;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct PromptSessionContext<'a> {
pub user_memory_block: Option<&'a str>,
pub goal_objective: Option<&'a str>,
pub project_context_pack_enabled: bool,
pub locale_tag: &'a str,
pub translation_enabled: bool,
pub model_id: &'a str,
}
impl Default for PromptSessionContext<'_> {
fn default() -> Self {
Self {
user_memory_block: None,
goal_objective: None,
project_context_pack_enabled: true,
locale_tag: "en",
translation_enabled: false,
model_id: "codewhale",
}
}
}
pub const HANDOFF_RELATIVE_PATH: &str = ".deepseek/handoff.md";
const INSTRUCTIONS_FILE_MAX_BYTES: usize = 100 * 1024;
fn translation_output_instruction(locale_tag: &str) -> String {
let target_language = translation_target_language_for_tag(locale_tag);
format!(
"\
## Language Output Requirement\n\
\n\
The user requires all responses in {target_language}. \
Always respond in {target_language} — use natural, professional language for all \
explanations, code comments, summaries, and conversational turns. \
Only output English for:\n\
- Code identifiers (variable names, function names, file paths)\n\
- Technical terms that lack a standard translation in {target_language}\n\
- Code blocks the user explicitly requests in English\n\n\
This is a hard display requirement: the user does not read English, \
so any English prose in your response will block their decision-making."
)
}
fn translation_target_language_for_tag(locale_tag: &str) -> &'static str {
let normalized = locale_tag.trim().to_ascii_lowercase();
if normalized.starts_with("ja") {
"Japanese (日本語)"
} else if normalized.starts_with("zh-hant")
|| normalized.contains("-tw")
|| normalized.contains("-hk")
|| normalized.contains("-mo")
{
"Traditional Chinese (繁體中文)"
} else if normalized.starts_with("zh") {
"Simplified Chinese (简体中文)"
} else if normalized.starts_with("pt") {
"Brazilian Portuguese (Português do Brasil)"
} else {
"English"
}
}
fn render_environment_block(workspace: &Path, locale_tag: &str) -> String {
let deepseek_version = env!("CARGO_PKG_VERSION");
let platform = std::env::consts::OS;
let shell = std::env::var("SHELL").unwrap_or_else(|_| "unknown".to_string());
let pwd = workspace.display();
format!(
"## Environment\n\
\n\
- lang: {locale_tag}\n\
- deepseek_version: {deepseek_version}\n\
- platform: {platform}\n\
- shell: {shell}\n\
- pwd: {pwd}"
)
}
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 Relay\n\nThe previous session in this workspace left a relay artifact at `{HANDOFF_RELATIVE_PATH}`. 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{trimmed}"
))
}
pub const BASE_PROMPT: &str = include_str!("prompts/base.md");
pub(crate) fn locale_reinforcement_preamble(locale_tag: &str) -> Option<&'static str> {
match locale_tag {
"zh-Hans" | "zh-CN" | "zh" => Some(LOCALE_PREAMBLE_ZH_HANS),
"ja" | "ja-JP" => Some(LOCALE_PREAMBLE_JA),
"pt-BR" | "pt" => Some(LOCALE_PREAMBLE_PT_BR),
_ => None,
}
}
pub(crate) fn locale_reinforcement_closer(locale_tag: &str) -> Option<&'static str> {
match locale_tag {
"zh-Hans" | "zh-CN" | "zh" => Some(LOCALE_CLOSER_ZH_HANS),
"ja" | "ja-JP" => Some(LOCALE_CLOSER_JA),
"pt-BR" | "pt" => Some(LOCALE_CLOSER_PT_BR),
_ => None,
}
}
const LOCALE_PREAMBLE_ZH_HANS: &str = "## 语言要求\n\n\
你正在 codewhale 中运行。无论任务上下文(代码、错误日志、文件名)\
是英文,无论系统提示的其余部分是英文,你都必须用简体中文进行 \
`reasoning_content`(内部思考)和最终回复。代码、文件路径、工具名称\
(例如 `read_file`、`exec_shell`)、环境变量、命令行参数和 URL \
保持原样 —— 只有自然语言散文要切换到简体中文。\n\n\
如果用户在会话中切换到另一种语言,从下一轮开始跟随切换。\
如果用户明确要求(例如 \"think in English\"),则覆盖此规则。";
const LOCALE_PREAMBLE_JA: &str = "## 言語要件\n\n\
codewhale を実行しています。タスクコンテキスト(コード、エラーログ、\
ファイル名)が英語であっても、システムプロンプトの他の部分が英語で\
あっても、`reasoning_content`(内部思考)と最終的な返信は日本語で\
行ってください。コード、ファイルパス、ツール名(例:`read_file`、\
`exec_shell`)、環境変数、コマンドライン引数、URL は元のまま —— \
自然言語の文章のみ日本語に切り替えます。\n\n\
ユーザーがセッション中に別の言語に切り替えた場合は、次のターンから\
それに従ってください。ユーザーが明示的に要求した場合(例:\
\"think in English\")はこのルールを上書きします。";
const LOCALE_PREAMBLE_PT_BR: &str = "## Requisito de Idioma\n\n\
Você está rodando dentro do codewhale. Escreva tanto \
`reasoning_content` (seu pensamento interno) quanto a resposta final \
em português do Brasil, mesmo quando o contexto da tarefa (código, \
logs de erro, nomes de arquivos) estiver em inglês e mesmo quando o \
resto do system prompt for em inglês. Mantenha código, caminhos de \
arquivos, nomes de ferramentas (por exemplo `read_file`, \
`exec_shell`), variáveis de ambiente, flags de linha de comando e \
URLs no formato original — apenas a prosa em linguagem natural muda \
para português do Brasil.\n\n\
Se o usuário mudar de idioma no meio da sessão, mude no próximo turno. \
Se o usuário pedir explicitamente (por exemplo, \"think in English\"), \
isso sobrescreve esta regra.";
const LOCALE_CLOSER_ZH_HANS: &str = "## 语言再次提醒\n\n\
**重要:你的 `reasoning_content`(内部思考)和最终回复必须保持简体中文。** \
无论你在这次会话中读到了多少英文代码、错误日志或文档,无论项目上下文 \
是英文,思考过程不能漂移到英文。这是会话级硬性要求 —— 用户的语言决定 \
你的语言,与上下文中累积的英文内容无关。除非用户明确要求切换(例如 \
\"think in English\"),否则继续用简体中文思考和回答。";
const LOCALE_CLOSER_JA: &str = "## 言語再確認\n\n\
**重要:`reasoning_content`(内部思考)と最終的な返信は日本語で行ってください。** \
このセッションで読み込んだ英語のコード、エラーログ、ドキュメントの量に \
関係なく、プロジェクトコンテキストが英語であっても、思考プロセスを \
英語に逸らさないでください。これはセッションレベルの厳格な要件であり、 \
ユーザーの言語があなたの言語を決定します。ユーザーが明示的に切り替えを \
要求しない限り(例:\"think in English\")、日本語で思考し、回答し続けて \
ください。";
const LOCALE_CLOSER_PT_BR: &str = "## Reforço de Idioma\n\n\
**Importante: seu `reasoning_content` (pensamento interno) e a resposta \
final devem permanecer em português do Brasil.** Independentemente de \
quanto código em inglês, logs de erro ou documentação você ler nesta \
sessão, e independentemente de o contexto do projeto ser em inglês, o \
processo de pensamento não pode derivar para o inglês. Este é um \
requisito rígido em nível de sessão — o idioma do usuário define seu \
idioma. A menos que o usuário peça explicitamente a troca (por exemplo, \
\"think in English\"), continue pensando e respondendo em português do \
Brasil.";
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 MEMORY_GUIDANCE: &str = include_str!("prompts/memory_guidance.md");
pub const AGENT_PROMPT: &str = include_str!("prompts/agent.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 default_approval_mode_for_mode(mode: AppMode) -> ApprovalMode {
match mode {
AppMode::Agent => ApprovalMode::Suggest,
AppMode::Yolo => ApprovalMode::Auto,
AppMode::Plan => ApprovalMode::Never,
}
}
fn approval_prompt_for_mode(mode: AppMode, approval_mode: ApprovalMode) -> &'static str {
match mode {
AppMode::Yolo => AUTO_APPROVAL,
AppMode::Plan => NEVER_APPROVAL,
AppMode::Agent => match approval_mode {
ApprovalMode::Auto => AUTO_APPROVAL,
ApprovalMode::Suggest => SUGGEST_APPROVAL,
ApprovalMode::Never => NEVER_APPROVAL,
},
}
}
fn apply_model_template(prompt: &str, model_id: &str) -> String {
prompt.replace("{model_id}", model_id)
}
const AUTHORITY_RECAP: &str = "\
## Authority Recap
The Constitution of CodeWhale (Articles I-VII) governs your behavior.
Tier 1 rules — truthfulness, user agency, tool-use mandate, verification
duty — are non-negotiable. The user's next message is the highest
directive within Constitutional bounds. Personality, memory, and handoff
context are subordinate to the Constitution, the Statutes, and the user's
current request. When in doubt, consult Article VII: The Hierarchy of Law.";
pub fn compose_prompt(mode: AppMode, personality: Personality) -> String {
compose_prompt_with_approval(mode, personality, default_approval_mode_for_mode(mode))
}
pub fn compose_prompt_with_approval(
mode: AppMode,
personality: Personality,
approval_mode: ApprovalMode,
) -> String {
compose_prompt_with_approval_and_model(mode, personality, approval_mode, "codewhale")
}
pub fn compose_prompt_with_approval_and_model(
mode: AppMode,
personality: Personality,
approval_mode: ApprovalMode,
model_id: &str,
) -> String {
let parts: [&str; 4] = [
&apply_model_template(BASE_PROMPT.trim(), model_id),
personality.prompt().trim(),
mode_prompt(mode).trim(),
approval_prompt_for_mode(mode, approval_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)
}
fn compose_mode_prompt_with_approval(mode: AppMode, approval_mode: ApprovalMode) -> String {
compose_prompt_with_approval(mode, Personality::Calm, approval_mode)
}
fn compose_mode_prompt_with_approval_and_model(
mode: AppMode,
approval_mode: ApprovalMode,
model_id: &str,
) -> String {
compose_prompt_with_approval_and_model(mode, Personality::Calm, approval_mode, model_id)
}
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,
project_context_pack_enabled: true,
locale_tag: "en",
translation_enabled: false,
model_id: "codewhale",
},
)
}
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 {
system_prompt_for_mode_with_context_skills_session_and_approval(
mode,
workspace,
_working_set_summary,
skills_dir,
instructions,
session_context,
default_approval_mode_for_mode(mode),
)
}
pub fn system_prompt_for_mode_with_context_skills_session_and_approval(
mode: AppMode,
workspace: &Path,
_working_set_summary: Option<&str>,
skills_dir: Option<&Path>,
instructions: Option<&[PathBuf]>,
session_context: PromptSessionContext<'_>,
approval_mode: ApprovalMode,
) -> SystemPrompt {
let mode_prompt =
compose_mode_prompt_with_approval_and_model(mode, approval_mode, session_context.model_id);
let project_context = load_project_context_with_parents(workspace);
let preamble = locale_reinforcement_preamble(session_context.locale_tag);
let mut full_prompt = if let Some(project_block) = project_context.as_system_block() {
format!("{mode_prompt}\n\n{project_block}")
} else {
tracing::warn!("No project context available and auto-generation failed");
mode_prompt
};
if let Some(preamble) = preamble {
full_prompt = format!("{preamble}\n\n{full_prompt}");
}
if session_context.project_context_pack_enabled
&& let Some(pack) = crate::project_context::generate_project_context_pack(workspace)
{
full_prompt = format!("{full_prompt}\n\n{pack}");
}
full_prompt = format!(
"{full_prompt}\n\n{}",
render_environment_block(workspace, session_context.locale_tag),
);
if session_context.translation_enabled {
full_prompt = format!(
"{full_prompt}\n\n{}",
translation_output_instruction(session_context.locale_tag)
);
}
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 (>60% during sustained work), 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 stored on new user messages 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(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}\n\n{MEMORY_GUIDANCE}");
}
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()
);
}
if let Some(handoff_block) = load_handoff_block(workspace) {
full_prompt = format!("{full_prompt}\n\n{handoff_block}");
}
full_prompt = format!("{full_prompt}\n\n{AUTHORITY_RECAP}");
if let Some(closer) = locale_reinforcement_closer(session_context.locale_tag) {
full_prompt = format!("{full_prompt}\n\n{closer}");
}
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)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
const HANDOFF_BLOCK_MARKER: &str = "left a relay artifact at `.deepseek/handoff.md`";
fn contains_cjk(text: &str) -> bool {
text.chars().any(|ch| {
matches!(
ch,
'\u{3040}'..='\u{30ff}'
| '\u{3400}'..='\u{4dbf}'
| '\u{4e00}'..='\u{9fff}'
| '\u{f900}'..='\u{faff}'
)
})
}
#[test]
fn base_prompt_carries_execution_discipline_block() {
for tag in [
"<tool_persistence>",
"<mandatory_tool_use>",
"<act_dont_ask>",
"<verification>",
"<missing_context>",
] {
assert!(
BASE_PROMPT.contains(tag),
"BASE_PROMPT missing required tag {tag}"
);
}
assert!(
BASE_PROMPT.contains("Tool-use enforcement"),
"BASE_PROMPT missing the tool-use enforcement clause"
);
}
#[test]
fn base_prompt_carries_constitutional_preamble() {
for phrase in [
"We begin with Brother Whale",
"Brother Whale is the founding intelligence",
"Every model that runs here is Brother Whale",
"future intelligences can better coordinate",
"Article II — The Primacy of Truth",
"Article VII — The Hierarchy of Law",
] {
assert!(
BASE_PROMPT.contains(phrase),
"BASE_PROMPT missing Constitutional phrase {phrase:?}"
);
}
}
#[test]
fn constitutional_hierarchy_keeps_case_command_above_local_law() {
let case_at = BASE_PROMPT
.find("2. **Case Command.**")
.expect("case command tier present");
let statute_at = BASE_PROMPT
.find("3. **Statutes.**")
.expect("statutes tier present");
let local_law_at = BASE_PROMPT
.find("5. **Local Law.**")
.expect("local law tier present");
assert!(
case_at < statute_at && statute_at < local_law_at,
"Article VII must keep the current user request above runtime guidance and local law"
);
assert!(
BASE_PROMPT.contains("actual runtime gates still determine what tools can execute"),
"Article VII must distinguish prompt authority from executable runtime gates"
);
}
#[test]
fn base_prompt_contains_model_id_template() {
assert!(
BASE_PROMPT.contains("{model_id}"),
"BASE_PROMPT must contain the {{model_id}} template for dynamic injection"
);
}
#[test]
fn apply_model_template_replaces_placeholder() {
let result = apply_model_template("You are {model_id}", "deepseek-v4-pro");
assert_eq!(result, "You are deepseek-v4-pro");
assert!(!result.contains("{model_id}"));
}
#[test]
fn compose_prompt_injects_model_id() {
let prompt = compose_prompt_with_approval_and_model(
AppMode::Agent,
Personality::Calm,
ApprovalMode::Suggest,
"deepseek-v4-flash",
);
assert!(
prompt.contains("You are deepseek-v4-flash"),
"composed prompt must contain the injected model id"
);
assert!(
!prompt.contains("{model_id}"),
"composed prompt must not contain the raw template placeholder"
);
}
#[test]
fn authority_recap_appears_in_full_prompt() {
let tmp = tempdir().expect("tempdir");
let text = match system_prompt_for_mode_with_context_skills_session_and_approval(
AppMode::Agent,
tmp.path(),
None,
None,
None,
PromptSessionContext::default(),
ApprovalMode::Suggest,
) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert!(
text.contains("## Authority Recap"),
"full system prompt must contain the authority recap"
);
assert!(
text.contains("The Constitution of CodeWhale (Articles I-VII) governs your behavior"),
"authority recap must reference the Constitution"
);
}
#[test]
fn calm_personality_declares_tier_8_subordination() {
assert!(
CALM_PERSONALITY.contains("Tier 8"),
"Calm personality must identify as Tier 8"
);
assert!(
CALM_PERSONALITY.contains("cannot override"),
"Calm personality must have a subordination clause"
);
}
#[test]
fn execution_discipline_is_at_the_end_for_cache_stability() {
let body = BASE_PROMPT;
let persistence_at = body
.find("<tool_persistence>")
.expect("tool_persistence anchor present");
let language_at = body.find("## Language").expect("Language anchor present");
assert!(
language_at < persistence_at,
"execution-discipline block must come after the early sections"
);
}
#[test]
fn plan_mode_prompt_uses_update_plan_as_confirmation_handoff() {
assert!(
PLAN_MODE.contains("call `update_plan`"),
"Plan mode must tell the model to finish plans through update_plan"
);
assert!(
PLAN_MODE.contains("accept / revise / exit prompt"),
"Plan mode must explain why update_plan is the UI handoff signal"
);
}
#[test]
fn render_environment_block_lists_supplied_locale_and_workspace() {
let tmp = tempdir().expect("tempdir");
let block = render_environment_block(tmp.path(), "zh-Hans");
assert!(block.starts_with("## Environment"));
assert!(block.contains("- lang: zh-Hans"));
assert!(block.contains(&format!(
"- deepseek_version: {}",
env!("CARGO_PKG_VERSION")
)));
assert!(block.contains(&format!("- pwd: {}", tmp.path().display())));
assert!(block.contains("- platform:"));
assert!(block.contains("- shell:"));
}
#[test]
fn locale_reinforcement_preamble_returns_native_script_for_supported_locales() {
assert!(locale_reinforcement_preamble("en").is_none());
assert!(locale_reinforcement_preamble("en-US").is_none());
assert!(locale_reinforcement_preamble("fr-FR").is_none());
assert!(locale_reinforcement_preamble("").is_none());
for tag in ["zh-Hans", "zh-CN", "zh"] {
let preamble =
locale_reinforcement_preamble(tag).expect("zh-Hans preamble should exist");
assert!(
preamble.contains("简体中文"),
"zh preamble must be in Simplified Chinese: {preamble:?}"
);
assert!(
preamble.contains("reasoning_content"),
"zh preamble must steer reasoning_content: {preamble:?}"
);
assert!(
preamble.contains("read_file"),
"zh preamble must call out tool-name immutability: {preamble:?}"
);
}
let ja = locale_reinforcement_preamble("ja").expect("ja preamble");
assert!(ja.contains("日本語"), "ja preamble must be in Japanese");
assert!(ja.contains("reasoning_content"));
let pt = locale_reinforcement_preamble("pt-BR").expect("pt-BR preamble");
assert!(
pt.contains("português do Brasil"),
"pt preamble must call out pt-BR explicitly"
);
assert!(pt.contains("reasoning_content"));
}
#[test]
fn system_prompt_prepends_locale_preamble_for_zh_hans() {
let tmp = tempdir().expect("tempdir");
let text = match system_prompt_for_mode_with_context_skills_session_and_approval(
AppMode::Agent,
tmp.path(),
None,
None,
None,
PromptSessionContext {
user_memory_block: None,
goal_objective: None,
project_context_pack_enabled: false,
locale_tag: "zh-Hans",
translation_enabled: false,
model_id: "codewhale",
},
ApprovalMode::Suggest,
) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
let preamble_marker = "## 语言要求";
let base_marker = "You are codewhale";
let preamble_pos = text
.find(preamble_marker)
.expect("zh-Hans preamble should be present");
let base_pos = text
.find(base_marker)
.expect("base prompt should be present");
assert!(
preamble_pos < base_pos,
"locale preamble must precede the English base prompt (preamble={preamble_pos}, base={base_pos})",
);
}
#[test]
fn locale_reinforcement_closer_returns_native_script_for_supported_locales() {
assert!(locale_reinforcement_closer("en").is_none());
assert!(locale_reinforcement_closer("fr-FR").is_none());
assert!(locale_reinforcement_closer("").is_none());
let zh = locale_reinforcement_closer("zh-Hans").expect("zh closer");
assert!(
zh.contains("简体中文"),
"zh closer must be in Simplified Chinese"
);
assert!(
zh.contains("reasoning_content"),
"zh closer must steer reasoning_content"
);
let ja = locale_reinforcement_closer("ja").expect("ja closer");
assert!(ja.contains("日本語"), "ja closer must be in Japanese");
assert!(ja.contains("reasoning_content"));
let pt = locale_reinforcement_closer("pt-BR").expect("pt-BR closer");
assert!(pt.contains("português do Brasil"));
assert!(pt.contains("reasoning_content"));
}
#[test]
fn system_prompt_bookends_zh_hans_with_preamble_and_closer() {
let tmp = tempdir().expect("tempdir");
let text = match system_prompt_for_mode_with_context_skills_session_and_approval(
AppMode::Agent,
tmp.path(),
None,
None,
None,
PromptSessionContext {
user_memory_block: None,
goal_objective: None,
project_context_pack_enabled: false,
locale_tag: "zh-Hans",
translation_enabled: false,
model_id: "codewhale",
},
ApprovalMode::Suggest,
) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
let preamble_pos = text
.find("## 语言要求")
.expect("zh-Hans preamble must be in prompt");
let closer_pos = text
.find("## 语言再次提醒")
.expect("zh-Hans closer must be in prompt");
assert!(
preamble_pos < closer_pos,
"closer must come after preamble (preamble={preamble_pos}, closer={closer_pos})",
);
let closer_header_end = closer_pos + "## 语言再次提醒".len();
let after_closer_body = &text[closer_header_end..];
assert!(
!after_closer_body.contains("\n## "),
"no other top-level section should follow the closer; got: {after_closer_body:?}",
);
}
#[test]
fn system_prompt_skips_locale_preamble_for_english() {
let tmp = tempdir().expect("tempdir");
let text = match system_prompt_for_mode_with_context_skills_session_and_approval(
AppMode::Agent,
tmp.path(),
None,
None,
None,
PromptSessionContext {
user_memory_block: None,
goal_objective: None,
project_context_pack_enabled: false,
locale_tag: "en",
translation_enabled: false,
model_id: "codewhale",
},
ApprovalMode::Suggest,
) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert!(
!text.contains("语言要求"),
"English locale must not get a zh preamble: {text:?}"
);
assert!(
!text.contains("言語要件"),
"English locale must not get a ja preamble: {text:?}"
);
assert!(
!text.contains("Requisito de Idioma"),
"English locale must not get a pt-BR preamble: {text:?}"
);
assert!(
!text.contains("语言再次提醒"),
"English locale must not get a zh closer: {text:?}"
);
assert!(
!text.contains("言語再確認"),
"English locale must not get a ja closer: {text:?}"
);
assert!(
!text.contains("Reforço de Idioma"),
"English locale must not get a pt-BR closer: {text:?}"
);
assert!(
!contains_cjk(&text),
"English system prompt should avoid native-script priming tokens: {text:?}"
);
}
#[test]
fn language_section_carries_reasoning_content_directives_for_1118() {
let lang = BASE_PROMPT;
assert!(
lang.contains("reasoning_content"),
"language section must explicitly call out reasoning_content"
);
assert!(
lang.contains("latest user message"),
"latest user message must be the primary language signal"
);
assert!(
lang.contains("clearly English") && lang.contains("must stay English"),
"English user turns must stay English even after localized context"
);
assert!(
lang.contains("Simplified Chinese")
&& lang.contains("must both be in Simplified Chinese"),
"Chinese user turns must still steer reasoning_content and replies"
);
assert!(
lang.contains("README.zh-CN.md") && lang.contains("tool results"),
"localized docs and tool results must be named as non-language signals"
);
for phrase in ["think in English", "reason in Chinese"] {
assert!(
lang.contains(phrase),
"expected the user-override example `{phrase}`"
);
}
}
#[test]
fn environment_block_is_inserted_into_system_prompt() {
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: None,
project_context_pack_enabled: true,
locale_tag: "ja",
translation_enabled: false,
model_id: "codewhale",
},
) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert!(prompt.contains("## Environment"));
assert!(prompt.contains("- lang: ja"));
assert!(prompt.contains("- deepseek_version:"));
}
#[test]
fn memory_guidance_carries_paired_examples() {
assert!(MEMORY_GUIDANCE.contains("declarative facts"));
assert!(MEMORY_GUIDANCE.contains(" ✓"));
assert!(MEMORY_GUIDANCE.contains(" ✗"));
assert!(MEMORY_GUIDANCE.contains("Imperative"));
}
#[test]
fn memory_guidance_absent_when_no_memory_block() {
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: None,
project_context_pack_enabled: false,
locale_tag: "en",
translation_enabled: false,
model_id: "codewhale",
},
) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert!(
!prompt.contains("Memory Hygiene"),
"memory guidance must not leak into sessions without a memory block"
);
}
#[test]
fn memory_guidance_appended_after_memory_block() {
let tmp = tempdir().expect("tempdir");
let block = "## User Memory\n\n- prefers Rust\n";
let prompt = match system_prompt_for_mode_with_context_skills_and_session(
AppMode::Agent,
tmp.path(),
None,
None,
None,
PromptSessionContext {
user_memory_block: Some(block),
goal_objective: None,
project_context_pack_enabled: false,
locale_tag: "en",
translation_enabled: false,
model_id: "codewhale",
},
) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
let mem_at = prompt.find("User Memory").expect("user memory present");
let guide_at = prompt.find("Memory Hygiene").expect("guidance present");
assert!(
mem_at < guide_at,
"guidance must come after the user memory block"
);
}
#[test]
fn memory_guidance_matches_constitutional_tier_order() {
let guidance = MEMORY_GUIDANCE
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
let current_request_at = guidance
.find("the user's current request (Tier 2)")
.expect("current request tier present");
let statutes_at = guidance
.find("Statutes (Tier 3)")
.expect("statutes tier present");
let local_law_at = guidance
.find("Local Law (Tier 5)")
.expect("local law tier present");
let live_evidence_at = guidance
.find("live evidence (Tier 6)")
.expect("live evidence tier present");
assert!(
current_request_at < statutes_at
&& statutes_at < local_law_at
&& local_law_at < live_evidence_at,
"memory guidance must keep the current request above memory and local law"
);
}
#[test]
fn project_context_pack_can_be_disabled() {
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join("README.md"), "# Pack test").expect("write readme");
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: None,
project_context_pack_enabled: false,
locale_tag: "en",
translation_enabled: false,
model_id: "codewhale",
},
) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert!(!prompt.contains("<project_context_pack>"));
}
#[test]
fn project_context_pack_is_before_dynamic_tail() {
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join("README.md"), "# Pack test").expect("write readme");
std::fs::create_dir_all(tmp.path().join(".deepseek")).expect("mkdir");
std::fs::write(tmp.path().join(".deepseek").join("handoff.md"), "handoff")
.expect("handoff");
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: None,
project_context_pack_enabled: true,
locale_tag: "en",
translation_enabled: false,
model_id: "codewhale",
},
) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert!(prompt.contains("<project_context_pack>"));
assert!(
prompt.find("<project_context_pack>").expect("pack")
< prompt.find("## Previous Session Relay").expect("relay")
);
}
#[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 relay — 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 codewhale"));
assert!(prompt.contains("Personality: Calm"));
assert!(prompt.contains("Mode: Agent"));
assert!(prompt.contains("Approval Policy: Suggest"));
}
#[test]
fn changelog_entry_exists_for_current_package_version() {
let version = env!("CARGO_PKG_VERSION");
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let Some(changelog_path) = manifest_dir
.ancestors()
.map(|dir| dir.join("CHANGELOG.md"))
.find(|candidate| candidate.is_file())
else {
eprintln!(
"changelog_entry_exists_for_current_package_version: no \
CHANGELOG.md found above {} — skipping (this gate only \
fires inside a workspace checkout).",
manifest_dir.display()
);
return;
};
let contents = std::fs::read_to_string(&changelog_path).unwrap_or_else(|err| {
panic!(
"failed to read CHANGELOG.md at {}: {err}",
changelog_path.display()
)
});
let header = format!("## [{version}]");
assert!(
contents.contains(&header),
"CHANGELOG.md is missing a `{header}` entry for the current package \
version. Add a release section at the top before tagging — see \
docs/RELEASE_CHECKLIST.md."
);
}
#[test]
fn compose_prompt_deterministic_order() {
let prompt = compose_prompt(AppMode::Yolo, Personality::Calm);
let base_pos = prompt.find("You are codewhale").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 agent_prompt_can_reflect_never_approval_policy() {
let prompt =
compose_prompt_with_approval(AppMode::Agent, Personality::Calm, ApprovalMode::Never);
assert!(prompt.contains("Mode: Agent"));
assert!(prompt.contains("Approval Policy: Never"));
assert!(prompt.contains("/config approval_mode suggest"));
}
#[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 Relay"));
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_below_compact_template() {
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"),
project_context_pack_enabled: true,
locale_tag: "en",
translation_enabled: false,
model_id: "codewhale",
},
) {
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 Relay").expect("compact block");
assert!(prompt.contains("Fix transcript corruption"));
assert!(compact_pos < goal_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(" "),
project_context_pack_enabled: true,
locale_tag: "en",
translation_enabled: false,
model_id: "codewhale",
},
) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert!(!prompt.contains("<session_goal>"));
assert!(!prompt.contains("## Current Session Goal"));
}
#[test]
fn tool_selection_guide_avoids_defensive_tool_suppression() {
let prompt = compose_prompt(AppMode::Agent, Personality::Calm);
assert!(prompt.contains("Tool Selection Guide"));
assert!(prompt.contains("Use `agent_eval`"));
assert!(
!prompt.contains("When NOT to use certain tools"),
"the system prompt should steer tool choice without training the model to avoid available tools"
);
assert!(
!prompt.contains("Don't reach for"),
"avoid defensive anti-tool wording in the base prompt"
);
}
#[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 language_mirroring_prioritizes_latest_user_message_over_locale_default() {
let prompt = compose_prompt(AppMode::Agent, Personality::Calm);
assert!(
prompt.contains("latest user message first"),
"the language directive must choose the turn language from the user message before \
falling back to the environment locale"
);
assert!(
prompt.contains("If the latest user message is clearly English"),
"English user text must not drift after non-English context"
);
assert!(
prompt.contains("localized READMEs") && prompt.contains("tool results"),
"file/tool context must not become a language signal"
);
assert!(
prompt.contains("even when the `lang` field in `## Environment` is `en`"),
"Chinese user text must override an English resolved locale for reasoning_content"
);
assert!(
prompt.contains("Use the `lang` field only when"),
"environment locale should be an ambiguity fallback, not the primary language source"
);
}
#[test]
fn english_base_prompt_avoids_native_script_language_priming() {
let prompt = compose_prompt(AppMode::Agent, Personality::Calm);
assert!(
!contains_cjk(&prompt),
"English base prompt should keep native-script reinforcement in locale bookends only"
);
assert!(
!prompt.contains("multilingual coding agent"),
"identity should not prime language switching; language belongs in the Language section"
);
}
#[test]
fn rlm_specialty_tool_guidance_present() {
let prompt = compose_prompt(AppMode::Agent, Personality::Calm);
assert!(prompt.contains("RLM — How 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}"
);
assert!(
!prompt.contains("When NOT to use RLM"),
"RLM guidance should explain fit and verification without telling the model to avoid the tool"
);
}
#[test]
fn workspace_orientation_guidance_present() {
let prompt = compose_prompt(AppMode::Agent, Personality::Calm);
assert!(prompt.contains("AGENTS.md"));
assert!(prompt.contains("Local Law"));
assert!(
prompt.contains("CLAUDE.md"),
"CLAUDE.md must be listed as a project instruction source"
);
}
#[test]
fn prompt_uses_persistent_agent_and_rlm_surface() {
let prompt = compose_prompt(AppMode::Agent, Personality::Calm);
for tool in [
"agent_open",
"agent_eval",
"agent_close",
"rlm_open",
"rlm_eval",
"rlm_configure",
"rlm_close",
"handle_read",
] {
assert!(
prompt.contains(tool),
"prompt should mention new persistent tool `{tool}`"
);
}
for retired in [
"agent_spawn",
"agent_wait",
"agent_result",
"agent_send_input",
"agent_assign",
"agent_resume",
"agent_list",
"spawn_agent",
"delegate_to_agent",
"send_input",
"close_agent",
] {
assert!(
!prompt.contains(retired),
"prompt should not advertise retired sub-agent tool `{retired}`"
);
}
}
#[test]
fn prompt_documents_fork_context_prefix_cache_contract() {
let prompt = compose_prompt(AppMode::Agent, Personality::Calm);
assert!(prompt.contains("fork_context: true"));
assert!(prompt.contains("byte-identical"));
assert!(prompt.contains("DeepSeek prefix-cache reuse"));
assert!(prompt.contains("Fresh sessions are the default"));
}
#[test]
fn subagent_done_sentinel_section_present() {
let prompt = compose_prompt(AppMode::Agent, Personality::Calm);
assert!(prompt.contains("Internal Sub-agent Completion Events"));
assert!(prompt.contains("<codewhale:subagent.done>"));
assert!(prompt.contains("not user input"));
assert!(prompt.contains("Integration protocol"));
assert!(prompt.contains("Do not tell the user they pasted sentinels"));
}
#[test]
fn preamble_rhythm_section_present() {
let prompt = compose_prompt(AppMode::Agent, Personality::Calm);
assert!(prompt.contains("In preambles, name the action"));
assert!(prompt.contains("Reading the module tree"));
}
#[test]
fn legacy_constants_still_available() {
assert!(AGENT_PROMPT.lines().next().is_some());
}
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 relay\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), "relay must be embedded");
assert!(a.contains("Finish #280."), "relay 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 Relay")
.expect("compaction relay template present");
let handoff_pos = prompt
.find(HANDOFF_BLOCK_MARKER)
.expect("relay 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 relay block"
);
assert!(
compact_pos < handoff_pos,
"## Compaction Relay must precede the relay 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"
);
}
}