#![allow(dead_code)]
use crate::models::{SystemBlock, SystemPrompt};
use crate::project_context::load_project_context_with_parents;
use std::path::{Path, PathBuf};
use std::sync::{LazyLock, Mutex};
pub mod base_preview;
pub(crate) mod text;
#[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,
pub context_window_override: Option<u32>,
pub verbosity: Option<&'a str>,
pub skills_scan_codewhale_only: bool,
pub plugin_registry: Option<&'a crate::plugins::PluginRegistry>,
pub mode: crate::tui::app::AppMode,
}
impl Default for PromptSessionContext<'_> {
fn default() -> Self {
Self {
user_memory_block: None,
goal_objective: None,
project_context_pack_enabled: false,
locale_tag: "en",
translation_enabled: false,
model_id: "codewhale",
context_window_override: None,
verbosity: None,
skills_scan_codewhale_only: false,
plugin_registry: None,
mode: crate::tui::app::AppMode::Agent,
}
}
}
pub const HANDOFF_RELATIVE_PATH: &str = ".codewhale/handoff.md";
const LEGACY_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 concise_output_discipline_instruction() -> &'static str {
"\
## Concise Output Discipline
To minimize token usage and optimize speed:
- Output only direct, actionable code, technical steps, or final answers.
- Eliminate all conversational filler, fluff, introductions, transitions, or summarizing conclusions.
- Do NOT explain what you are about to do or what you have just completed.
- Do NOT provide conversational status updates before or after running tools.
- Keep explanations and comments extremely brief and technical, explaining only non-obvious reasoning."
}
fn is_concise_verbosity(value: Option<&str>) -> bool {
value.is_some_and(|v| v.trim().eq_ignore_ascii_case("concise"))
}
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 if normalized.starts_with("es") {
"Latin American Spanish (Español latinoamericano)"
} else if normalized.starts_with("vi") {
"Vietnamese (Tiếng Việt)"
} else if normalized.starts_with("ko") {
"Korean (한국어)"
} else if normalized.starts_with("ca") {
"Catalan (Català)"
} else if normalized.starts_with("de") {
"German (Deutsch)"
} else if normalized.starts_with("fr") {
"French (Français)"
} else if normalized.starts_with("id") {
"Indonesian (Bahasa Indonesia)"
} else if normalized.starts_with("hi") {
"Hindi (हिन्दी)"
} else if normalized.starts_with("ru") {
"Russian (Русский)"
} else if normalized.starts_with("uk") {
"Ukrainian (Українська)"
} else {
"English"
}
}
pub(crate) fn render_environment_block(_workspace: &Path, locale_tag: &str) -> String {
let platform = std::env::consts::OS;
let shell = crate::shell_dispatcher::global_dispatcher()
.kind()
.binary()
.to_string();
format!(
"## Environment\n\
\n\
- lang: {locale_tag}\n\
- platform: {platform}\n\
- shell: {shell}"
)
}
#[derive(Debug, Clone)]
pub enum InstructionSource {
File(PathBuf),
Inline { name: String, content: String },
}
impl From<PathBuf> for InstructionSource {
fn from(path: PathBuf) -> Self {
InstructionSource::File(path)
}
}
impl From<&PathBuf> for InstructionSource {
fn from(path: &PathBuf) -> Self {
InstructionSource::File(path.clone())
}
}
fn render_instructions_block(sources: &[InstructionSource]) -> Option<String> {
let mut sections: Vec<String> = Vec::new();
for source in sources {
let (raw_source_name, raw_content): (String, String) = match source {
InstructionSource::File(path) => match std::fs::read_to_string(path) {
Ok(raw) => (path.display().to_string(), raw),
Err(err) => {
tracing::warn!(
target: "instructions",
?err,
?path,
"skipping unreadable instructions file"
);
continue;
}
},
InstructionSource::Inline { name, content } => (name.clone(), content.clone()),
};
let trimmed = raw_content.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[…truncated: {} of {} bytes omitted — consider splitting this instructions file]",
&trimmed[..head_end],
trimmed.len() - head_end,
trimmed.len()
)
} else {
trimmed.to_string()
};
sections.push(format!(
"<instructions source=\"{raw_source_name}\">\n{body}\n</instructions>"
));
}
if sections.is_empty() {
None
} else {
Some(sections.join("\n\n"))
}
}
fn load_handoff_block(workspace: &Path) -> Option<String> {
let primary = workspace.join(HANDOFF_RELATIVE_PATH);
let path = if primary.exists() {
primary
} else {
workspace.join(LEGACY_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(crate) fn load_user_constitution_block() -> Option<String> {
if user_constitution_disabled_by_setup_state() {
return None;
}
let path = match codewhale_config::UserConstitution::path() {
Ok(path) => path,
Err(err) => {
tracing::warn!(
target: "prompts",
"could not resolve user-global constitution path: {err:#}"
);
return None;
}
};
match codewhale_config::UserConstitution::load_from(&path) {
codewhale_config::UserConstitutionLoad::Loaded(constitution) => {
constitution.render_block(None)
}
codewhale_config::UserConstitutionLoad::Missing
| codewhale_config::UserConstitutionLoad::Empty => None,
codewhale_config::UserConstitutionLoad::Invalid(err) => {
tracing::warn!(
target: "prompts",
"skipping invalid user-global constitution {}: {err}",
path.display()
);
None
}
codewhale_config::UserConstitutionLoad::Unreadable(err) => {
tracing::warn!(
target: "prompts",
"skipping unreadable user-global constitution {}: {err}",
path.display()
);
None
}
}
}
fn user_constitution_disabled_by_setup_state() -> bool {
match codewhale_config::SetupState::load() {
Ok(Some(state)) => matches!(
state.constitution_choice,
codewhale_config::ConstitutionChoice::Bundled
| codewhale_config::ConstitutionChoice::Deferred
| codewhale_config::ConstitutionChoice::ExpertOverride
),
Ok(None) => false,
Err(err) => {
tracing::warn!(
target: "prompts",
"could not resolve setup-state path while loading user constitution: {err:#}"
);
false
}
}
}
#[cfg(test)]
use text::CALM_PERSONALITY;
pub use text::{
BASE_PROMPT, COMPACT_TEMPLATE, CORE_EXECUTION_PROFILE_PROMPT, GOAL_CONTINUATION_PROMPT,
HEADLESS_BASE_PROMPT, LANGUAGE_PROMPT, MEMORY_GUIDANCE, OUTPUT_PROMPT,
};
static BASE_PROMPT_OVERRIDE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
static LOCALE_PREAMBLE_ZH_HANS_OVERRIDE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
static LOCALE_PREAMBLE_JA_OVERRIDE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
static LOCALE_PREAMBLE_PT_BR_OVERRIDE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
static LOCALE_PREAMBLE_VI_OVERRIDE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
static LOCALE_CLOSER_ZH_HANS_OVERRIDE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
static LOCALE_CLOSER_JA_OVERRIDE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
static LOCALE_CLOSER_PT_BR_OVERRIDE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
static LOCALE_CLOSER_VI_OVERRIDE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
static AUTHORITY_RECAP_OVERRIDE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
static STATIC_PROMPT_COMPOSER: std::sync::OnceLock<Box<StaticPromptComposer>> =
std::sync::OnceLock::new();
static PROMPT_OVERRIDE_NOTICES: LazyLock<Mutex<Vec<String>>> =
LazyLock::new(|| Mutex::new(Vec::new()));
#[non_exhaustive]
#[derive(Debug)]
pub struct StaticPromptCtx<'a> {
pub model_id: &'a str,
pub personality: Personality,
pub default_layers: &'a str,
}
pub type StaticPromptComposer = dyn Fn(&StaticPromptCtx<'_>) -> String + Send + Sync + 'static;
pub fn set_base_prompt_override(s: String) -> Result<(), String> {
set_prompt_override(&BASE_PROMPT_OVERRIDE, s)
}
pub const CONSTITUTION_OVERRIDE_FILE: &str = "prompts/constitution.md";
pub const BASE_PROMPT_OVERRIDE_OPT_IN_ENV: &str = "CODEWHALE_ALLOW_BASE_PROMPT_OVERRIDE";
pub(crate) fn base_prompt_override_opt_in() -> bool {
match std::env::var(BASE_PROMPT_OVERRIDE_OPT_IN_ENV) {
Ok(v) => matches!(
v.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "on" | "yes"
),
Err(_) => false,
}
}
fn read_prompt_override_file(config_dir: &Path, relative: &str) -> Option<String> {
let path = config_dir.join(relative);
let raw = std::fs::read_to_string(&path).ok()?;
if raw.trim().is_empty() {
tracing::warn!(
target: "prompts",
"ignoring empty prompt override file {}",
path.display(),
);
return None;
}
tracing::info!(
target: "prompts",
"loaded prompt override from {}",
path.display(),
);
Some(raw)
}
fn push_prompt_override_notice(message: String) {
if let Ok(mut notices) = PROMPT_OVERRIDE_NOTICES.lock() {
notices.push(message);
}
}
pub fn take_prompt_override_notices() -> Vec<String> {
PROMPT_OVERRIDE_NOTICES
.lock()
.map(|mut notices| std::mem::take(&mut *notices))
.unwrap_or_default()
}
pub fn load_config_dir_prompt_overrides(config_dir: &Path) -> Vec<&'static str> {
let mut applied = Vec::new();
if let Some(text) = read_prompt_override_file(config_dir, CONSTITUTION_OVERRIDE_FILE) {
if !base_prompt_override_opt_in() {
let warning = format!(
"Custom Constitution override found at {}/{} but {} is not set; using the bundled Constitution. Set {}=1 to opt in.",
config_dir.display(),
CONSTITUTION_OVERRIDE_FILE,
BASE_PROMPT_OVERRIDE_OPT_IN_ENV,
BASE_PROMPT_OVERRIDE_OPT_IN_ENV,
);
tracing::warn!(
target: "prompts",
"{warning}",
);
push_prompt_override_notice(warning);
} else if set_base_prompt_override(text).is_ok() {
applied.push("constitution");
}
}
applied
}
pub fn load_prompt_overrides_from_config_home() {
let Ok(home) = codewhale_config::codewhale_home() else {
return;
};
let applied = load_config_dir_prompt_overrides(&home);
if !applied.is_empty() {
tracing::info!(
target: "prompts",
"applied {} config-directory prompt override(s): {}",
applied.len(),
applied.join(", "),
);
}
}
fn set_prompt_override(cell: &std::sync::OnceLock<String>, s: String) -> Result<(), String> {
cell.set(s)
}
fn effective_prompt_override<'a>(
cell: &'a std::sync::OnceLock<String>,
fallback: &'static str,
) -> &'a str {
cell.get().map(String::as_str).unwrap_or(fallback)
}
fn effective_base_prompt() -> &'static str {
effective_prompt_override(&BASE_PROMPT_OVERRIDE, BASE_PROMPT)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BasePromptOrigin {
Bundled,
ConfigOverride,
}
impl BasePromptOrigin {
pub(crate) fn label(self) -> &'static str {
match self {
Self::Bundled => "bundled in this codewhale-tui build (BASE_PROMPT, compiled in)",
Self::ConfigOverride => concat!(
"config-directory override installed at startup ",
"(prompts/constitution.md, opt-in enabled)"
),
}
}
}
pub(crate) fn base_prompt_origin() -> BasePromptOrigin {
if BASE_PROMPT_OVERRIDE.get().is_some() {
BasePromptOrigin::ConfigOverride
} else {
BasePromptOrigin::Bundled
}
}
pub(crate) fn effective_base_prompt_text() -> &'static str {
effective_base_prompt()
}
#[must_use]
pub fn effective_base_prompt_source(config_dir: Option<&Path>) -> base_preview::BasePromptSource {
if STATIC_PROMPT_COMPOSER.get().is_some() {
return base_preview::BasePromptSource::EmbedderComposer;
}
if BASE_PROMPT_OVERRIDE.get().is_some() {
return base_preview::BasePromptSource::ConfigOverride {
path: config_dir.map_or_else(
|| CONSTITUTION_OVERRIDE_FILE.to_string(),
|dir| dir.join(CONSTITUTION_OVERRIDE_FILE).display().to_string(),
),
};
}
base_preview::BasePromptSource::Bundled
}
fn effective_static_prompt_composer() -> Option<&'static StaticPromptComposer> {
STATIC_PROMPT_COMPOSER.get().map(Box::as_ref)
}
fn effective_locale_preamble_zh_hans() -> &'static str {
effective_prompt_override(&LOCALE_PREAMBLE_ZH_HANS_OVERRIDE, LOCALE_PREAMBLE_ZH_HANS)
}
fn effective_locale_preamble_ja() -> &'static str {
effective_prompt_override(&LOCALE_PREAMBLE_JA_OVERRIDE, LOCALE_PREAMBLE_JA)
}
fn effective_locale_preamble_pt_br() -> &'static str {
effective_prompt_override(&LOCALE_PREAMBLE_PT_BR_OVERRIDE, LOCALE_PREAMBLE_PT_BR)
}
fn effective_locale_preamble_vi() -> &'static str {
effective_prompt_override(&LOCALE_PREAMBLE_VI_OVERRIDE, LOCALE_PREAMBLE_VI)
}
fn effective_locale_closer_zh_hans() -> &'static str {
effective_prompt_override(&LOCALE_CLOSER_ZH_HANS_OVERRIDE, LOCALE_CLOSER_ZH_HANS)
}
fn effective_locale_closer_ja() -> &'static str {
effective_prompt_override(&LOCALE_CLOSER_JA_OVERRIDE, LOCALE_CLOSER_JA)
}
fn effective_locale_closer_pt_br() -> &'static str {
effective_prompt_override(&LOCALE_CLOSER_PT_BR_OVERRIDE, LOCALE_CLOSER_PT_BR)
}
fn effective_locale_closer_vi() -> &'static str {
effective_prompt_override(&LOCALE_CLOSER_VI_OVERRIDE, LOCALE_CLOSER_VI)
}
pub(crate) fn effective_authority_recap() -> &'static str {
effective_prompt_override(&AUTHORITY_RECAP_OVERRIDE, AUTHORITY_RECAP)
}
pub(crate) fn locale_reinforcement_preamble(locale_tag: &str) -> Option<&'static str> {
match locale_tag {
"zh-Hans" | "zh-CN" | "zh" => Some(effective_locale_preamble_zh_hans()),
"ja" | "ja-JP" => Some(effective_locale_preamble_ja()),
"pt-BR" | "pt" => Some(effective_locale_preamble_pt_br()),
"vi" | "vi-VN" => Some(effective_locale_preamble_vi()),
_ => None,
}
}
pub(crate) fn locale_reinforcement_closer(locale_tag: &str) -> Option<&'static str> {
match locale_tag {
"zh-Hans" | "zh-CN" | "zh" => Some(effective_locale_closer_zh_hans()),
"ja" | "ja-JP" => Some(effective_locale_closer_ja()),
"pt-BR" | "pt" => Some(effective_locale_closer_pt_br()),
"vi" | "vi-VN" => Some(effective_locale_closer_vi()),
_ => None,
}
}
const LOCALE_PREAMBLE_ZH_HANS: &str = "## 语言要求\n\n\
你正在 codewhale 中运行。无论任务上下文(代码、错误日志、文件名)\
是英文,无论系统提示的其余部分是英文,你都必须用简体中文进行 \
`reasoning_content`(内部思考)和最终回复。代码、文件路径、工具名称\
(例如 `File`、`Bash`)、环境变量、命令行参数和 URL \
保持原样 —— 只有自然语言散文要切换到简体中文。\n\n\
如果用户在会话中切换到另一种语言,从下一轮开始跟随切换。\
如果用户明确要求(例如 \"think in English\"),则覆盖此规则。";
const LOCALE_PREAMBLE_JA: &str = "## 言語要件\n\n\
codewhale を実行しています。タスクコンテキスト(コード、エラーログ、\
ファイル名)が英語であっても、システムプロンプトの他の部分が英語で\
あっても、`reasoning_content`(内部思考)と最終的な返信は日本語で\
行ってください。コード、ファイルパス、ツール名(例:`File`、\
`Bash`)、環境変数、コマンドライン引数、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 `File`, \
`Bash`), 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.";
const LOCALE_PREAMBLE_VI: &str = "## Yêu cầu ngôn ngữ\n\n\
Bạn đang chạy trong codewhale. Cho dù ngữ cảnh tác vụ (mã nguồn, nhật ký lỗi, tên tệp) \
là tiếng Anh, cho dù phần còn lại của system prompt là tiếng Anh, bạn đều phải sử dụng \
tiếng Việt cho phần `reasoning_content` (suy nghĩ nội bộ) và câu trả lời cuối cùng. Các từ \
mã nguồn, đường dẫn tệp, tên công cụ (ví dụ `File`, `Bash`), biến môi trường, \
tham số dòng lệnh và URL giữ nguyên dạng gốc —— chỉ các văn bản giải thích bằng ngôn ngữ \
tự nhiên mới được chuyển sang tiếng Việt.\n\n\
Nếu người dùng chuyển sang ngôn ngữ khác trong phiên làm việc, hãy chuyển theo từ lượt tiếp theo. \
Nếu người dùng yêu cầu rõ ràng (ví dụ \"think in English\"), hãy ghi đè quy tắc này.";
const LOCALE_CLOSER_VI: &str = "## Nhắc nhở ngôn ngữ một lần nữa\n\n\
**Quan trọng: phần `reasoning_content` (suy nghĩ nội bộ) và phản hồi cuối cùng của bạn phải được viết bằng tiếng Việt.** \
Dù bạn có đọc bao nhiêu mã nguồn tiếng Anh, nhật ký lỗi hay tài liệu trong phiên làm việc này, và dù ngữ cảnh \
dự án có là tiếng Anh, quá trình suy nghĩ của bạn cũng không được chuyển sang tiếng Anh. Đây là yêu cầu cứng \
ở cấp phiên làm việc —— ngôn ngữ của người dùng quyết định ngôn ngữ của bạn, không phụ thuộc vào nội dung tiếng Anh \
tích lũy trong ngữ cảnh. Trừ khi người dùng yêu cầu rõ ràng việc chuyển đổi (ví dụ \"think in English\"), \
hãy tiếp tục suy nghĩ và trả lời bằng tiếng Việt.";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Personality {
Calm,
}
fn apply_model_template(
prompt: &str,
model_id: &str,
_context_window_override: Option<u32>,
) -> String {
prompt.replace("{model_id}", model_id)
}
const AUTHORITY_RECAP: &str = "\
## Authority Recap
Codewhale's constitution governs your behavior. Ground truth underlies the
whole list: the user may override a fact, but no one may invent one. When
guidance conflicts, consult ### Whose word wins — that is the only place
precedence is stated.";
pub(crate) fn compose_prompt_with_approval_model_and_shell(
personality: Personality,
model_id: &str,
) -> String {
let default_layers = compose_default_static_layers(personality, model_id);
apply_static_prompt_composer(
effective_static_prompt_composer(),
personality,
model_id,
&default_layers,
)
}
pub(crate) fn compose_default_static_layers(_personality: Personality, model_id: &str) -> String {
compose_default_static_layers_with_context(model_id, None)
}
fn compose_default_static_layers_with_context(
model_id: &str,
context_window_override: Option<u32>,
) -> String {
let layers = format!(
"{}\n\n{}\n\n{}",
effective_base_prompt().trim(),
LANGUAGE_PROMPT.trim(),
OUTPUT_PROMPT.trim()
);
apply_model_template(&layers, model_id, context_window_override)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PromptHost {
Interactive,
Headless,
}
fn apply_static_prompt_composer(
composer: Option<&StaticPromptComposer>,
personality: Personality,
model_id: &str,
default_layers: &str,
) -> String {
match composer {
Some(composer) => composer(&StaticPromptCtx {
model_id,
personality,
default_layers,
}),
None => default_layers.to_string(),
}
}
pub fn system_prompt_for_mode_with_context(
workspace: &Path,
working_set_summary: Option<&str>,
) -> SystemPrompt {
system_prompt_for_mode_with_context_and_skills(workspace, working_set_summary, None, None, None)
}
pub fn system_prompt_for_mode_with_context_and_skills(
workspace: &Path,
working_set_summary: Option<&str>,
skills_dir: Option<&Path>,
instructions: Option<&[InstructionSource]>,
user_memory_block: Option<&str>,
) -> SystemPrompt {
system_prompt_for_mode_with_context_skills_and_session(
workspace,
working_set_summary,
skills_dir,
instructions,
PromptSessionContext {
user_memory_block,
goal_objective: None,
project_context_pack_enabled: false,
locale_tag: "en",
translation_enabled: false,
model_id: "codewhale",
context_window_override: None,
verbosity: None,
skills_scan_codewhale_only: false,
plugin_registry: None,
mode: crate::tui::app::AppMode::Agent,
},
)
}
pub fn system_prompt_for_mode_with_context_skills_and_session(
workspace: &Path,
_working_set_summary: Option<&str>,
skills_dir: Option<&Path>,
instructions: Option<&[InstructionSource]>,
session_context: PromptSessionContext<'_>,
) -> SystemPrompt {
system_prompt_for_mode_with_context_skills_session_and_approval(
workspace,
_working_set_summary,
skills_dir,
instructions,
session_context,
)
}
pub fn system_prompt_for_mode_with_context_skills_session_and_approval(
workspace: &Path,
_working_set_summary: Option<&str>,
skills_dir: Option<&Path>,
instructions: Option<&[InstructionSource]>,
session_context: PromptSessionContext<'_>,
) -> SystemPrompt {
system_prompt_for_mode_with_context_skills_session_and_approval_for_host(
workspace,
_working_set_summary,
skills_dir,
instructions,
session_context,
PromptHost::Interactive,
)
}
pub(crate) fn system_prompt_for_mode_with_context_skills_session_and_approval_for_host(
workspace: &Path,
_working_set_summary: Option<&str>,
skills_dir: Option<&Path>,
instructions: Option<&[InstructionSource]>,
session_context: PromptSessionContext<'_>,
prompt_host: PromptHost,
) -> SystemPrompt {
let bundled_headless = prompt_host == PromptHost::Headless
&& BASE_PROMPT_OVERRIDE.get().is_none()
&& effective_static_prompt_composer().is_none();
let composed = if bundled_headless {
apply_model_template(
HEADLESS_BASE_PROMPT.trim(),
session_context.model_id,
session_context.context_window_override,
)
} else {
let default_layers = compose_default_static_layers_with_context(
session_context.model_id,
session_context.context_window_override,
);
apply_static_prompt_composer(
effective_static_prompt_composer(),
Personality::Calm,
session_context.model_id,
&default_layers,
)
};
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!("{}\n\n{project_block}", composed.trim())
} else {
tracing::warn!("No project context available and auto-generation failed");
composed
};
if let Some(preamble) = preamble {
full_prompt = format!("{preamble}\n\n{full_prompt}");
}
if let Some(user_constitution_block) = load_user_constitution_block() {
full_prompt = format!("{full_prompt}\n\n{user_constitution_block}");
}
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}");
}
if session_context.translation_enabled {
full_prompt = format!(
"{full_prompt}\n\n{}",
translation_output_instruction(session_context.locale_tag)
);
}
if is_concise_verbosity(session_context.verbosity) {
full_prompt = format!(
"{full_prompt}\n\n{}",
concise_output_discipline_instruction()
);
}
let skill_discovery_mode = crate::skills::SkillDiscoveryMode::from_codewhale_only(
session_context.skills_scan_codewhale_only,
);
let skills_block = match skills_dir {
Some(dir) => {
crate::skills::render_available_skills_context_for_workspace_and_dir_with_mode_and_plugins(
workspace,
dir,
skill_discovery_mode,
session_context.locale_tag,
session_context.plugin_registry,
)
}
None => crate::skills::render_available_skills_context_for_workspace_with_mode_and_plugins(
workspace,
skill_discovery_mode,
session_context.locale_tag,
session_context.plugin_registry,
),
};
if let Some(block) = skills_block {
full_prompt = format!("{full_prompt}\n\n{block}");
}
if !bundled_headless {
full_prompt.push_str("\n\n");
full_prompt.push_str(CORE_EXECUTION_PROFILE_PROMPT.trim());
}
let mut workspace_parts = vec![render_environment_block(
workspace,
session_context.locale_tag,
)];
if let Some(memory_block) = session_context.user_memory_block
&& !memory_block.trim().is_empty()
{
workspace_parts.push(format!("{memory_block}\n\n{MEMORY_GUIDANCE}"));
}
if prompt_host == PromptHost::Interactive
&& let Some(harness_block) = crate::continual_harness::prompt_block(workspace)
{
workspace_parts.push(harness_block);
}
if let Some(goal_objective) = session_context.goal_objective
&& !goal_objective.trim().is_empty()
{
workspace_parts.push(format!(
"## Current Goal\n\n<session_goal>\n{}\n</session_goal>",
goal_objective.trim()
));
}
let workspace_body = workspace_parts.join("\n\n");
let permissions_body = instructions.and_then(render_instructions_block);
let route_body = render_route_fragment(&session_context);
let token_budget_body = load_handoff_block(workspace);
let mut world_state = world_state_from_session_facts(
Some(workspace_body.as_str()),
permissions_body.as_deref(),
Some(route_body.as_str()),
None, None, token_budget_body.as_deref(),
);
if let Some(fragment) =
codewhale_core::fragments::load_additional_project_instruction_fragment(workspace)
{
world_state = world_state.with_project_instructions(fragment.content);
debug_assert!(world_state.validate_caps().is_ok());
}
let mut blocks = crate::model_context::WorldStateSnapshot {
constitution: full_prompt,
world_state,
}
.to_system_blocks();
if !bundled_headless {
blocks.push(SystemBlock {
block_type: "text".to_string(),
text: effective_authority_recap().trim().to_string(),
cache_control: None,
});
}
if let Some(closer) = locale_reinforcement_closer(session_context.locale_tag) {
blocks.push(SystemBlock {
block_type: "text".to_string(),
text: closer.trim().to_string(),
cache_control: None,
});
}
SystemPrompt::Blocks(blocks)
}
#[must_use]
pub fn system_prompt_flat_text(prompt: &SystemPrompt) -> String {
match prompt {
SystemPrompt::Text(text) => text.clone(),
SystemPrompt::Blocks(blocks) => blocks
.iter()
.map(|block| block.text.as_str())
.collect::<Vec<_>>()
.join("\n\n"),
}
}
fn render_route_fragment(session_context: &PromptSessionContext<'_>) -> String {
let verbosity = session_context
.verbosity
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("default");
format!(
"verbosity: {verbosity}\ntranslation: {}",
if session_context.translation_enabled {
"on"
} else {
"off"
},
)
}
pub fn world_state_from_session_facts(
workspace_body: Option<&str>,
permissions_body: Option<&str>,
route_body: Option<&str>,
agent_topology_body: Option<&str>,
skills_tools_body: Option<&str>,
token_budget_body: Option<&str>,
) -> crate::model_context::WorldState {
let mut state = crate::model_context::WorldState::new();
if let Some(body) = workspace_body.filter(|s| !s.trim().is_empty()) {
state = state.with_workspace(body);
}
if let Some(body) = permissions_body.filter(|s| !s.trim().is_empty()) {
state = state.with_permissions(body);
}
if let Some(body) = route_body.filter(|s| !s.trim().is_empty()) {
state = state.with_route(body);
}
if let Some(body) = agent_topology_body.filter(|s| !s.trim().is_empty()) {
state = state.with_agent_topology(body);
}
if let Some(body) = skills_tools_body.filter(|s| !s.trim().is_empty()) {
state = state.with_skills_tools(body);
}
if let Some(body) = token_budget_body.filter(|s| !s.trim().is_empty()) {
state = state.with_token_budget(body);
}
state
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::apply_patch::ApplyPatchTool;
use crate::tools::file::{EditFileTool, WriteFileTool};
use crate::tools::handle::HandleReadTool;
use crate::tools::rlm::RlmTool;
use crate::tools::shell::BashTool;
use crate::tools::spec::ToolSpec;
use tempfile::tempdir;
const HANDOFF_BLOCK_MARKER: &str = "left a relay artifact at `.codewhale/handoff.md`";
#[test]
fn config_override_reads_present_nonempty_file() {
let tmp = tempdir().expect("tempdir");
let prompts_dir = tmp.path().join("prompts");
std::fs::create_dir_all(&prompts_dir).expect("mkdir");
std::fs::write(
prompts_dir.join("constitution.md"),
"You are a long-form writing companion.\n",
)
.expect("write override");
let got = read_prompt_override_file(tmp.path(), CONSTITUTION_OVERRIDE_FILE);
assert_eq!(
got.as_deref(),
Some("You are a long-form writing companion.\n")
);
}
#[test]
fn config_override_absent_file_falls_back() {
let tmp = tempdir().expect("tempdir");
assert!(read_prompt_override_file(tmp.path(), CONSTITUTION_OVERRIDE_FILE).is_none());
}
#[test]
fn config_override_requires_explicit_opt_in() {
let _env_guard = crate::test_support::lock_test_env();
let tmp = tempdir().expect("tempdir");
let prompts_dir = tmp.path().join("prompts");
std::fs::create_dir_all(&prompts_dir).expect("mkdir");
std::fs::write(
prompts_dir.join("constitution.md"),
"You are a long-form writing companion.\n",
)
.expect("write override");
assert!(read_prompt_override_file(tmp.path(), CONSTITUTION_OVERRIDE_FILE).is_some());
if std::env::var(BASE_PROMPT_OVERRIDE_OPT_IN_ENV).is_err() {
let _ = take_prompt_override_notices();
assert!(
load_config_dir_prompt_overrides(tmp.path()).is_empty(),
"override must require the explicit opt-in flag, not just a file"
);
let notices = take_prompt_override_notices();
assert!(
notices
.iter()
.any(|notice| notice.contains(BASE_PROMPT_OVERRIDE_OPT_IN_ENV)
&& notice.contains("using the bundled Constitution")),
"gated override should record a visible notice, got {notices:?}"
);
}
}
#[test]
fn config_override_empty_file_is_ignored() {
let tmp = tempdir().expect("tempdir");
let prompts_dir = tmp.path().join("prompts");
std::fs::create_dir_all(&prompts_dir).expect("mkdir");
std::fs::write(prompts_dir.join("constitution.md"), " \n\t\n").expect("write blank");
assert!(read_prompt_override_file(tmp.path(), CONSTITUTION_OVERRIDE_FILE).is_none());
}
#[test]
fn prompt_override_storage_reports_duplicate_sets() {
let cell = std::sync::OnceLock::new();
assert_eq!(effective_prompt_override(&cell, "fallback"), "fallback");
assert!(set_prompt_override(&cell, "first".to_string()).is_ok());
assert_eq!(effective_prompt_override(&cell, "fallback"), "first");
assert_eq!(
set_prompt_override(&cell, "second".to_string()),
Err("second".to_string())
);
assert_eq!(effective_prompt_override(&cell, "fallback"), "first");
}
#[test]
fn static_prompt_composer_unset_keeps_default_layers_byte_identical() {
let default_layers = compose_default_static_layers(Personality::Calm, "deepseek-v4-flash");
let composed = apply_static_prompt_composer(
None,
Personality::Calm,
"deepseek-v4-flash",
&default_layers,
);
assert_byte_identical("unset static prompt composer", &default_layers, &composed);
}
#[test]
fn static_prompt_composer_receives_context_and_replaces_layers() {
let default_layers = compose_default_static_layers(Personality::Calm, "deepseek-v4-pro");
let composer: Box<StaticPromptComposer> = Box::new(|ctx| {
assert_eq!(ctx.model_id, "deepseek-v4-pro");
assert_eq!(ctx.personality, Personality::Calm);
assert!(ctx.default_layers.contains("You are Codewhale"));
assert!(
ctx.default_layers
.contains("Take the work seriously. Don't take")
);
assert!(!ctx.default_layers.contains("## Core Tool Taxonomy"));
assert!(!ctx.default_layers.contains("Approval Policy"));
"embedder static prompt".to_string()
});
let composed = apply_static_prompt_composer(
Some(composer.as_ref()),
Personality::Calm,
"deepseek-v4-pro",
&default_layers,
);
assert_eq!(composed, "embedder static prompt");
}
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 bundled_headless_contract_is_small_and_direct() {
for phrase in [
"You already have an A",
"begin from possibility",
"bring your whole attention",
"a question, idea, or task",
"Invent no urgency or deadline",
"tools as senses",
"active authority is your limit",
"Failure is information",
"Check\nbefore concluding",
"unverified work as complete",
] {
assert!(
HEADLESS_BASE_PROMPT.contains(phrase),
"bundled headless contract missing {phrase:?}"
);
}
for ceremony in [
"todo_write",
"checklist",
"`repl`",
"workflow",
"Fleet",
"sub-agent",
"delegation",
"goals",
"harness",
"Mode:",
] {
assert!(
!HEADLESS_BASE_PROMPT.contains(ceremony),
"bundled headless contract must leave optional capabilities to the tool catalog: {ceremony:?}"
);
}
assert!(
HEADLESS_BASE_PROMPT.split_whitespace().count() <= 75,
"bundled headless contract must stay compact"
);
}
#[test]
fn every_mode_shares_one_prompt_per_host() {
let _env_lock = crate::test_support::lock_test_env();
let tmp = tempdir().expect("tempdir");
std::fs::write(
tmp.path().join("AGENTS.md"),
"# Project instruction\nPreserve the blue-ocean marker.\n",
)
.expect("write project instruction");
for host in [PromptHost::Interactive, PromptHost::Headless] {
let prompts = [
crate::tui::app::AppMode::Plan,
crate::tui::app::AppMode::Agent,
crate::tui::app::AppMode::Operate,
]
.map(|mode| {
system_prompt_flat_text(
&system_prompt_for_mode_with_context_skills_session_and_approval_for_host(
tmp.path(),
None,
None,
None,
PromptSessionContext {
mode,
..PromptSessionContext::default()
},
host,
),
)
});
assert_eq!(prompts[0], prompts[1]);
assert_eq!(prompts[1], prompts[2]);
assert!(prompts[0].contains("Preserve the blue-ocean marker"));
assert!(!prompts[0].contains("##### Mode:"));
if host == PromptHost::Headless {
assert!(prompts[0].contains("You already have an A"));
assert!(!prompts[0].contains("## Core Execution"));
assert!(!prompts[0].contains("## Authority Recap"));
}
}
}
#[test]
fn base_prompt_carries_constitutional_core() {
for phrase in [
"## Codewhale",
"You are Codewhale",
"The A is already yours",
"Let the work speak",
"### Ground truth",
"### User intent and scope",
"### Truthful completion",
"### Put guarantees in mechanism",
"### Whose word wins",
] {
assert!(
BASE_PROMPT.contains(phrase),
"BASE_PROMPT missing Constitutional phrase {phrase:?}"
);
}
}
#[test]
fn constitutional_kernel_keeps_first_turn_authority_safety_and_completion() {
let fresh_prefix = compose_default_static_layers(Personality::Calm, "deepseek-v4-pro");
for phrase in [
"Do what the user's current request asks, no more.",
"require express user authorization in",
"otherwise name the decision and ask.",
"external publication, spending",
"credentials, and material scope expansion",
"prohibitions stay binding; convenience creates no exception",
"never route around it or claim prose granted",
"Nothing is done until checked.",
"Read test output, not only exit status",
"External actions are not complete until",
"Work still running is not complete",
"Never present a partial result as the whole.",
"no one may tell you to invent one",
"1. The user's request, this turn.",
"2. This constitution.",
] {
assert!(
fresh_prefix.contains(phrase),
"fresh constitution prefix missing kernel invariant {phrase:?}"
);
}
}
#[test]
fn procedural_playbooks_are_not_eager_constitution() {
let fresh_prefix = compose_default_static_layers(Personality::Calm, "deepseek-v4-pro");
for heading in [
"### Keep momentum",
"### Think in causes",
"### Honor constraints before preferences",
"### Skill and role constraints are binding",
"### Restraint",
"### Leave continuity",
] {
assert!(
!fresh_prefix.contains(heading),
"procedural playbook should stay outside the full fresh prefix: {heading:?}"
);
}
assert!(
!BASE_PROMPT.contains("## STATUTES (Tier 2)")
&& !BASE_PROMPT.contains("## REGULATIONS (Tier 3)"),
"the balanced Constitution must not restore the old procedural policy tail"
);
}
#[test]
fn base_prompt_carries_verify_then_stop_completion_contract() {
for phrase in [
"Nothing is done until checked.",
"Read test output, not only exit status",
"Work still running is not complete",
"Never present a partial result as the whole.",
] {
assert!(
BASE_PROMPT.contains(phrase),
"BASE_PROMPT missing completion-contract phrase {phrase:?}"
);
}
}
#[test]
fn yolo_mode_uses_the_shared_completion_contract() {
let tmp = tempdir().expect("tempdir");
let text = system_prompt_flat_text(
&system_prompt_for_mode_with_context_skills_session_and_approval(
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",
context_window_override: None,
verbosity: None,
skills_scan_codewhale_only: false,
plugin_registry: None,
mode: crate::tui::app::AppMode::Yolo,
},
),
);
for phrase in [
"### Truthful completion",
"Nothing is done until checked.",
"Never present a partial result as the whole.",
] {
assert!(
text.contains(phrase),
"YOLO-mode composed prompt missing completion-contract phrase {phrase:?}"
);
}
assert!(!text.contains("##### Mode:"));
}
#[test]
fn constitutional_hierarchy_keeps_user_turn_above_local_law() {
let heading_at = BASE_PROMPT
.find("### Whose word wins")
.expect("Whose word wins heading present");
let user_at = BASE_PROMPT
.find("1. The user's request, this turn.")
.expect("user request tier present");
let constitution_at = BASE_PROMPT
.find("2. This constitution.")
.expect("constitution tier present");
let project_at = BASE_PROMPT
.find("3. Project law and instructions")
.expect("project tier present");
let preference_at = BASE_PROMPT
.find("4. Your standing user-global preferences.")
.expect("user-global preference tier present");
let memory_at = BASE_PROMPT
.find("5. Memory and previous-session handoffs.")
.expect("memory/handoff tier present");
assert!(
heading_at < user_at
&& user_at < constitution_at
&& constitution_at < project_at
&& project_at < preference_at
&& preference_at < memory_at,
"Whose word wins must rank the current user request above constitution, \
project law, standing user-global preferences, then memory/handoffs"
);
assert!(
BASE_PROMPT.contains("the user may override a fact, but no one may invent\none"),
"Whose word wins must keep ground truth overridable but never inventable"
);
assert!(
BASE_PROMPT.contains("A tie you cannot break is not yours to break"),
"Whose word wins must keep tie-break escalation"
);
}
#[test]
fn base_prompt_is_model_fact_free() {
for placeholder in [
"{model_id}",
"{context_window_note}",
"{subagent_economics}",
"{model_thinking_note}",
"{model_characteristics}",
] {
assert!(
!BASE_PROMPT.contains(placeholder),
"0.9.0 BASE_PROMPT must not contain model-fact placeholder {placeholder}"
);
}
for forbidden in [
"Your V4 Characteristics",
"Model Characteristics",
"one-million-token context window",
"provider-dependent and not known",
] {
assert!(
!BASE_PROMPT.contains(forbidden),
"0.9.0 BASE_PROMPT must not contain model-specific fact {forbidden:?}"
);
}
}
fn assert_no_unresolved_model_placeholders(prompt: &str) {
for placeholder in [
"{model_id}",
"{context_window_note}",
"{subagent_economics}",
"{model_thinking_note}",
"{model_characteristics}",
] {
assert!(
!prompt.contains(placeholder),
"composed prompt must not contain unresolved {placeholder}"
);
}
}
#[test]
fn compose_prompt_for_v4_model_stays_model_fact_free() {
let prompt =
compose_prompt_with_approval_model_and_shell(Personality::Calm, "deepseek-v4-pro");
assert!(prompt.contains("You are Codewhale"));
assert!(!prompt.contains("Your V4 Characteristics"));
assert!(!prompt.contains("one-million-token context window"));
assert_no_unresolved_model_placeholders(&prompt);
}
#[test]
fn compose_prompt_for_kimi_stays_model_fact_free() {
let prompt =
compose_prompt_with_approval_model_and_shell(Personality::Calm, "moonshotai/kimi-k2.6");
assert!(prompt.contains("You are Codewhale"));
assert!(!prompt.contains("Your V4 Characteristics"));
assert!(!prompt.contains("one-million"));
assert!(!prompt.contains("$0.14"));
assert!(!prompt.contains("262144-token context window"));
assert!(!prompt.contains("Models may emit *thinking tokens*"));
assert_no_unresolved_model_placeholders(&prompt);
}
#[test]
fn compose_prompt_for_openai_api_gpt_55_stays_model_fact_free() {
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "gpt-5.5");
assert!(prompt.contains("You are Codewhale"));
assert!(!prompt.contains("Your V4 Characteristics"));
assert!(!prompt.contains("1050000-token context window"));
assert!(!prompt.contains("Models may emit *thinking tokens*"));
assert!(!prompt.contains("provider-dependent and not known"));
assert_no_unresolved_model_placeholders(&prompt);
}
#[test]
fn compose_prompt_for_unknown_model_stays_model_fact_free() {
let prompt =
compose_prompt_with_approval_model_and_shell(Personality::Calm, "llama3.3:70b");
assert!(prompt.contains("You are Codewhale"));
assert!(!prompt.contains("Your V4 Characteristics"));
assert!(!prompt.contains("one-million"));
assert!(!prompt.contains("$0.14"));
assert!(!prompt.contains("provider-dependent and not known"));
assert!(!prompt.contains("Models may emit *thinking tokens*"));
assert_no_unresolved_model_placeholders(&prompt);
}
#[test]
fn apply_model_template_replaces_placeholder() {
let result = apply_model_template("You are {model_id}", "deepseek-v4-pro", None);
assert_eq!(result, "You are deepseek-v4-pro");
assert!(!result.contains("{model_id}"));
}
#[test]
fn apply_model_template_does_not_resolve_removed_model_fact_templates() {
let result = apply_model_template("{context_window_note}", "gpt-5.5", Some(400_000));
assert_eq!(result, "{context_window_note}");
assert!(!result.contains("400000-token context window"));
assert!(!result.contains("1050000-token context window"));
}
#[test]
fn compose_prompt_is_model_agnostic_in_preamble() {
let flash =
compose_prompt_with_approval_model_and_shell(Personality::Calm, "deepseek-v4-flash");
let kimi =
compose_prompt_with_approval_model_and_shell(Personality::Calm, "moonshotai/kimi-k2.6");
assert!(
flash.contains("You are Codewhale"),
"0.9.0 preamble must open with the model-agnostic Codewhale stance"
);
assert!(
!flash.contains("You are deepseek-v4-flash")
&& !kimi.contains("You are moonshotai/kimi-k2.6"),
"0.9.0 preamble must not inject a per-model identity line"
);
assert!(
!flash.contains("{model_id}") && !kimi.contains("{model_id}"),
"composed prompt must not contain the raw {{model_id}} placeholder"
);
}
#[test]
fn tool_descriptions_carry_edit_and_shell_guidance() {
let write = WriteFileTool.description();
assert!(
write.contains("instead of heredocs")
&& write.contains("`Bash`")
&& !write.contains("exec_shell"),
"write guidance must name the live Bash tool and never the retired exec_shell name"
);
let edit = EditFileTool.description();
assert!(edit.contains("File `read`"));
assert!(edit.contains("File `patch` or `write`"));
assert!(
!edit.contains("read_file")
&& !edit.contains("write_file")
&& !edit.contains("apply_patch"),
"edit guidance must not teach a retired tool name: {edit:?}"
);
let patch = ApplyPatchTool.description();
assert!(patch.contains("unified-diff") && patch.contains("transactional"));
let shell_tool = BashTool::new("Bash");
let shell = shell_tool.description();
assert!(shell.contains("background=true"));
assert!(shell.contains(">5 seconds"));
}
#[test]
fn composed_prompt_does_not_claim_tool_availability() {
let prompt =
compose_prompt_with_approval_model_and_shell(Personality::Calm, "deepseek-v4-pro");
assert!(!prompt.contains("## Core Tool Taxonomy"));
assert!(!prompt.contains("## Toolbox"));
assert!(prompt.contains("You are Codewhale"));
}
#[test]
fn authority_recap_appears_in_full_prompt() {
let tmp = tempdir().expect("tempdir");
let text = system_prompt_flat_text(
&system_prompt_for_mode_with_context_skills_session_and_approval(
tmp.path(),
None,
None,
None,
PromptSessionContext::default(),
),
);
assert!(
text.contains("## Authority Recap"),
"full system prompt must contain the authority recap"
);
assert!(
text.contains("Codewhale's constitution governs your behavior"),
"authority recap must reference the Constitution"
);
assert!(
text.contains("consult ### Whose word wins"),
"authority recap must point at 0.9.0's precedence section"
);
}
#[test]
fn system_prompt_merges_workspace_and_configured_skills_dir() {
let _env_guard = crate::test_support::lock_test_env();
let tmp = tempdir().expect("tempdir");
let _home = ScopedHome::set(tmp.path().join("home"));
let workspace = tmp.path().join("workspace");
let configured_dir = tmp.path().join("configured-skills");
write_test_skill(
&workspace.join(".claude").join("skills"),
"workspace-skill",
"workspace skill",
);
write_test_skill(&configured_dir, "configured-skill", "configured skill");
let text = system_prompt_flat_text(&system_prompt_for_mode_with_context_and_skills(
&workspace,
None,
Some(&configured_dir),
None,
None,
));
assert!(text.contains("workspace-skill"));
assert!(text.contains("configured-skill"));
}
struct ScopedHome {
previous: Option<std::ffi::OsString>,
}
impl ScopedHome {
fn set(path: std::path::PathBuf) -> Self {
let previous = std::env::var_os("HOME");
unsafe {
std::env::set_var("HOME", path);
}
Self { previous }
}
}
impl Drop for ScopedHome {
fn drop(&mut self) {
unsafe {
if let Some(previous) = self.previous.take() {
std::env::set_var("HOME", previous);
} else {
std::env::remove_var("HOME");
}
}
}
}
fn write_test_skill(root: &std::path::Path, name: &str, description: &str) {
let dir = root.join(name);
std::fs::create_dir_all(&dir).expect("skill dir");
std::fs::write(
dir.join("SKILL.md"),
format!("---\nname: {name}\ndescription: {description}\n---\n\n# {name}\n"),
)
.expect("skill file");
}
#[test]
fn constitution_has_no_separate_personality_tier() {
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
assert!(
!prompt.contains("Personality: Calm — Tier 8"),
"Personality tier should not appear as a separate section"
);
assert!(
prompt.contains("Take the work seriously. Don't take"),
"Preamble should carry tone guidance (take the work, not yourself, seriously)"
);
assert!(prompt.contains("You are Codewhale"));
assert!(prompt.contains("Let the work speak"));
}
#[test]
fn render_environment_block_keeps_actionable_host_facts_without_version() {
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("- pwd:"));
assert!(!block.contains("- codewhale_version:"));
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("`File`"),
"zh preamble must call out tool-name immutability with a LIVE tool \
name; `read_file` is retired (registry.rs:2067): {preamble:?}"
);
assert!(
!preamble.contains("read_file") && !preamble.contains("exec_shell"),
"zh preamble must never teach a retired tool name: {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 = system_prompt_flat_text(
&system_prompt_for_mode_with_context_skills_session_and_approval(
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",
context_window_override: None,
verbosity: None,
skills_scan_codewhale_only: false,
plugin_registry: None,
mode: crate::tui::app::AppMode::Agent,
},
),
);
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 v092_locales_add_no_prompt_bookends_so_prompt_bytes_stay_stable() {
for tag in [
"zh-Hant", "ko", "es-419", "ca", "de", "fr", "id", "hi", "ru", "uk",
] {
assert!(
locale_reinforcement_preamble(tag).is_none(),
"{tag} must not gain a locale preamble"
);
assert!(
locale_reinforcement_closer(tag).is_none(),
"{tag} must not gain a locale closer"
);
}
for tag in ["zh-Hans", "ja", "pt-BR", "vi"] {
assert!(
locale_reinforcement_preamble(tag).is_some(),
"{tag} lost its locale preamble"
);
assert!(
locale_reinforcement_closer(tag).is_some(),
"{tag} lost its locale closer"
);
}
}
#[test]
fn translation_seam_names_every_shipped_locale_canonically() {
for locale in crate::localization::Locale::shipped() {
assert_eq!(
translation_target_language_for_tag(locale.tag()),
locale.translation_target_name(),
"{} translation seam drifted from the canonical locale name",
locale.tag()
);
}
}
#[test]
fn system_prompt_bookends_zh_hans_with_preamble_and_closer() {
let tmp = tempdir().expect("tempdir");
let text = system_prompt_flat_text(
&system_prompt_for_mode_with_context_skills_session_and_approval(
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",
context_window_override: None,
verbosity: None,
skills_scan_codewhale_only: false,
plugin_registry: None,
mode: crate::tui::app::AppMode::Agent,
},
),
);
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 = system_prompt_flat_text(
&system_prompt_for_mode_with_context_skills_session_and_approval(
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",
context_window_override: None,
verbosity: None,
skills_scan_codewhale_only: false,
plugin_registry: None,
mode: crate::tui::app::AppMode::Agent,
},
),
);
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(BASE_PROMPT),
"base prompt must not contain static CJK priming tokens"
);
}
#[test]
fn locale_bookends_carry_reasoning_content_directives_for_1118() {
let lang = LOCALE_PREAMBLE_ZH_HANS;
assert!(
lang.contains("reasoning_content"),
"locale preamble must explicitly call out reasoning_content"
);
assert!(
lang.contains("最终回复"),
"locale preamble must explicitly cover the final reply"
);
assert!(
lang.contains("代码") && lang.contains("工具名称"),
"code and tool names must be named as non-language signals"
);
assert!(
LOCALE_CLOSER_ZH_HANS.contains("reasoning_content")
&& LOCALE_CLOSER_ZH_HANS.contains("继续用简体中文思考和回答"),
"closing bookend must preserve recency-positioned language reinforcement"
);
let phrase = "think in English";
assert!(
lang.contains(phrase) && LOCALE_CLOSER_ZH_HANS.contains(phrase),
"expected the user-override example `{phrase}`"
);
}
#[test]
fn environment_block_is_inserted_into_system_prompt() {
let tmp = tempdir().expect("tempdir");
let prompt =
system_prompt_flat_text(&system_prompt_for_mode_with_context_skills_and_session(
tmp.path(),
None,
None,
None,
PromptSessionContext {
user_memory_block: None,
goal_objective: None,
project_context_pack_enabled: false,
locale_tag: "ja",
translation_enabled: false,
model_id: "codewhale",
context_window_override: None,
verbosity: None,
skills_scan_codewhale_only: false,
plugin_registry: None,
mode: crate::tui::app::AppMode::Agent,
},
));
assert!(prompt.contains("## Environment"));
assert!(prompt.contains("- lang: ja"));
assert!(!prompt.contains("- codewhale_version:"));
assert!(prompt.contains("- platform:"));
assert!(prompt.contains("- shell:"));
}
#[test]
fn user_global_constitution_block_is_injected_separately() {
let _env_guard = crate::test_support::lock_test_env();
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).expect("workspace dir");
let codewhale_home = tmp.path().join("codewhale-home");
std::fs::create_dir_all(&codewhale_home).expect("codewhale home");
let _codewhale_home =
crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", codewhale_home.as_os_str());
let constitution = codewhale_config::UserConstitution {
about: Some("Maintains Codewhale release lanes.".to_string()),
working_style: vec!["Prefer live verification before claims.".to_string()],
priorities: vec!["Keep release gates green.".to_string()],
autonomy_preference: codewhale_config::AutonomyPreference::Balanced,
..codewhale_config::UserConstitution::default()
};
constitution
.save_to(
&codewhale_home
.join(codewhale_config::user_constitution::USER_CONSTITUTION_FILE_NAME),
)
.expect("save user constitution");
let prompt =
system_prompt_flat_text(&system_prompt_for_mode_with_context_skills_and_session(
&workspace,
None,
None,
None,
PromptSessionContext {
project_context_pack_enabled: false,
..PromptSessionContext::default()
},
));
let base_at = prompt.find("### Whose word wins").expect("base prompt");
let user_block_at = prompt
.find("<codewhale_user_constitution")
.expect("user constitution block");
let env_at = prompt.find("- lang:").expect("rendered environment block");
assert!(
base_at < user_block_at && user_block_at < env_at,
"user constitution should be its own layer after the base/project context and before volatile environment data"
);
assert!(prompt.contains("source=\"user-global\""));
assert!(prompt.contains("Maintains Codewhale release lanes."));
assert!(prompt.contains("Prefer live verification before claims."));
assert!(
!prompt.contains(&codewhale_home.display().to_string()),
"prompt should use the stable user-global source label, not a device-specific home path"
);
}
#[test]
fn bundled_choice_disables_user_global_constitution_block() {
let _env_guard = crate::test_support::lock_test_env();
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).expect("workspace dir");
let codewhale_home = tmp.path().join("codewhale-home");
std::fs::create_dir_all(&codewhale_home).expect("codewhale home");
let _codewhale_home =
crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", codewhale_home.as_os_str());
let constitution = codewhale_config::UserConstitution {
about: Some("This file should stay inactive.".to_string()),
..codewhale_config::UserConstitution::default()
};
constitution
.save_to(
&codewhale_home
.join(codewhale_config::user_constitution::USER_CONSTITUTION_FILE_NAME),
)
.expect("save user constitution");
let mut state = codewhale_config::SetupState::default();
state.complete_constitution_checkpoint(
crate::tui::setup::CONSTITUTION_CHECKPOINT_VERSION,
codewhale_config::ConstitutionChoice::Bundled,
);
state
.save_to(&codewhale_home.join(codewhale_config::setup_state::SETUP_STATE_FILE_NAME))
.expect("save setup state");
let prompt =
system_prompt_flat_text(&system_prompt_for_mode_with_context_skills_and_session(
&workspace,
None,
None,
None,
PromptSessionContext {
project_context_pack_enabled: false,
..PromptSessionContext::default()
},
));
assert!(!prompt.contains("<codewhale_user_constitution"));
assert!(!prompt.contains("This file should stay inactive."));
}
#[test]
fn invalid_user_global_constitution_is_skipped() {
let _env_guard = crate::test_support::lock_test_env();
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).expect("workspace dir");
let codewhale_home = tmp.path().join("codewhale-home");
std::fs::create_dir_all(&codewhale_home).expect("codewhale home");
std::fs::write(
codewhale_home.join(codewhale_config::user_constitution::USER_CONSTITUTION_FILE_NAME),
"{ not valid json",
)
.expect("write invalid user constitution");
let _codewhale_home =
crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", codewhale_home.as_os_str());
let prompt =
system_prompt_flat_text(&system_prompt_for_mode_with_context_skills_and_session(
&workspace,
None,
None,
None,
PromptSessionContext {
project_context_pack_enabled: false,
..PromptSessionContext::default()
},
));
assert!(!prompt.contains("<codewhale_user_constitution"));
}
#[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_does_not_reference_scrapped_moraine() {
assert!(!MEMORY_GUIDANCE.contains("Moraine"));
assert!(!MEMORY_GUIDANCE.contains("moraine"));
}
#[test]
fn memory_guidance_absent_when_no_memory_block() {
let tmp = tempdir().expect("tempdir");
let prompt =
system_prompt_flat_text(&system_prompt_for_mode_with_context_skills_and_session(
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",
context_window_override: None,
verbosity: None,
skills_scan_codewhale_only: false,
plugin_registry: None,
mode: crate::tui::app::AppMode::Agent,
},
));
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 =
system_prompt_flat_text(&system_prompt_for_mode_with_context_skills_and_session(
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",
context_window_override: None,
verbosity: None,
skills_scan_codewhale_only: false,
plugin_registry: None,
mode: crate::tui::app::AppMode::Agent,
},
));
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 continual_harness_is_injected_as_untrusted_world_state() {
let tmp = tempdir().expect("tempdir");
crate::continual_harness::refine(
tmp.path(),
crate::continual_harness::HarnessRefinement {
kind: crate::continual_harness::HarnessEntryKind::PromptNote,
title: "Verify release claims from direct evidence".to_string(),
content: "Retain exact current command output for each release gate.".to_string(),
evidence:
"A prior release report mixed stale hosted CI with newer local test output."
.to_string(),
},
)
.expect("persist harness state");
let prompt =
system_prompt_flat_text(&system_prompt_for_mode_with_context_skills_and_session(
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",
context_window_override: None,
verbosity: None,
skills_scan_codewhale_only: false,
plugin_registry: None,
mode: crate::tui::app::AppMode::Agent,
},
));
assert!(prompt.contains("<continual_harness trust=\"untrusted\">"));
assert!(prompt.contains("supplemental working guidance"));
assert!(prompt.contains("Verify release claims from direct evidence"));
}
#[test]
fn headless_prompt_omits_continual_harness_guidance() {
let tmp = tempdir().expect("tempdir");
crate::continual_harness::refine(
tmp.path(),
crate::continual_harness::HarnessRefinement {
kind: crate::continual_harness::HarnessEntryKind::PromptNote,
title: "Use a project-specific orchestration routine".to_string(),
content: "This guidance is available through the harness tool.".to_string(),
evidence: "Prior interactive session.".to_string(),
},
)
.expect("persist harness state");
let prompt = system_prompt_flat_text(
&system_prompt_for_mode_with_context_skills_session_and_approval_for_host(
tmp.path(),
None,
None,
None,
PromptSessionContext::default(),
PromptHost::Headless,
),
);
assert!(!prompt.contains("<continual_harness"));
assert!(!prompt.contains("project-specific orchestration routine"));
}
#[test]
fn memory_guidance_does_not_state_precedence() {
let guidance = MEMORY_GUIDANCE.to_ascii_lowercase();
for forbidden in [
"tier 1",
"tier 2",
"tier 7",
"statute",
"regulation",
"local law",
"constitutional hierarchy",
] {
assert!(
!guidance.contains(forbidden),
"MEMORY_GUIDANCE must not restate ranks (found {forbidden:?})"
);
}
assert!(
MEMORY_GUIDANCE.contains("treated as a preference")
&& MEMORY_GUIDANCE.contains("not a command"),
"keep the imperative-as-preference rule"
);
}
#[test]
fn only_the_constitution_states_precedence() {
let overlays = [
("CALM_PERSONALITY", CALM_PERSONALITY),
("COMPACT_TEMPLATE", COMPACT_TEMPLATE),
("MEMORY_GUIDANCE", MEMORY_GUIDANCE),
("LANGUAGE_PROMPT", LANGUAGE_PROMPT),
("OUTPUT_PROMPT", OUTPUT_PROMPT),
("AUTHORITY_RECAP", AUTHORITY_RECAP),
];
let rank_markers = [
"Tier 1",
"Tier 2",
"Tier 3",
"Tier 4",
"Tier 5",
"Tier 6",
"Tier 7",
"Tier 8",
"Tier 9",
"Statute",
"Article IV",
"Article V",
"Article VII",
"Local Law",
"Regulation (Tier",
];
for (name, text) in overlays {
for marker in rank_markers {
assert!(
!text.contains(marker),
"{name} must not carry rank vocabulary {marker:?}"
);
}
}
assert!(
BASE_PROMPT.contains("### Whose word wins"),
"canonical precedence section must remain in BASE_PROMPT"
);
assert!(
BASE_PROMPT.contains("This ordering is stated here and nowhere else"),
"BASE_PROMPT must assert single-source precedence"
);
}
#[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 =
system_prompt_flat_text(&system_prompt_for_mode_with_context_skills_and_session(
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",
context_window_override: None,
verbosity: None,
skills_scan_codewhale_only: false,
plugin_registry: None,
mode: crate::tui::app::AppMode::Agent,
},
));
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 =
system_prompt_flat_text(&system_prompt_for_mode_with_context_skills_and_session(
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",
context_window_override: None,
verbosity: None,
skills_scan_codewhale_only: false,
plugin_registry: None,
mode: crate::tui::app::AppMode::Agent,
},
));
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 = system_prompt_flat_text(&system_prompt_for_mode_with_context(workspace, None));
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 =
system_prompt_flat_text(&system_prompt_for_mode_with_context(tmp.path(), None));
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 =
system_prompt_flat_text(&system_prompt_for_mode_with_context(tmp.path(), None));
assert!(!prompt.contains(HANDOFF_BLOCK_MARKER));
}
#[test]
fn compose_prompt_includes_all_layers() {
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
assert!(prompt.contains("## Codewhale"));
assert!(prompt.contains("### Whose word wins"));
assert!(!prompt.contains("## STATUTES (Tier 2)"));
assert!(!prompt.contains("## EVIDENCE (Tier 6)"));
assert!(!prompt.contains("Mode: Agent"));
assert!(!prompt.contains("Approval Policy:"));
}
#[test]
fn constitution_md_carries_required_structure() {
let md = BASE_PROMPT;
assert!(md.contains("## Codewhale"), "missing title");
let mut cursor = 0usize;
for needle in [
"## Codewhale",
"### Ground truth",
"### User intent and scope",
"### Truthful completion",
"### Put guarantees in mechanism",
"### Whose word wins",
] {
let pos = md
.find(needle)
.unwrap_or_else(|| panic!("ordering check: {needle:?} not found"));
assert!(
pos >= cursor,
"cache-stable ordering broken: {needle:?} at {pos} precedes a previous section at {cursor}"
);
cursor = pos + needle.len();
}
}
#[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_with_approval_model_and_shell(Personality::Calm, "codewhale");
let base_pos = prompt.find("## Codewhale").unwrap();
let article_pos = prompt.find("### Ground truth").unwrap();
assert!(base_pos < article_pos);
}
#[test]
fn base_prompt_is_mode_agnostic() {
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
assert!(!prompt.contains("Mode: Agent"));
assert!(!prompt.contains("Mode: YOLO"));
assert!(!prompt.contains("Mode: Plan"));
assert!(!prompt.contains("Approval Policy:"));
assert!(prompt.contains("You are Codewhale"));
assert!(prompt.contains("Take the work seriously. Don't take"));
}
#[test]
fn approval_policy_no_longer_inlined_in_base_prompt() {
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
assert!(!prompt.contains("Mode: Agent"));
assert!(!prompt.contains("Approval Policy:"));
assert!(prompt.contains("You are Codewhale"));
}
#[test]
fn execution_contract_states_proposal_is_not_execution() {
assert!(
CORE_EXECUTION_PROFILE_PROMPT.contains("is the proposal, not the execution"),
"Execution profile must state the propose-vs-execute contract"
);
assert!(
CORE_EXECUTION_PROFILE_PROMPT.contains("present the change in your plan"),
"Execution profile must name the correct behavior on rejection"
);
}
#[test]
fn personality_is_folded_into_constitution() {
let calm = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
assert!(!calm.contains("## Personality:"));
assert!(calm.contains("Take the work seriously. Don't take"));
assert!(calm.contains("You are Codewhale"));
}
#[test]
fn compact_template_is_lazy_in_fresh_prompt() {
let tmp = tempdir().expect("tempdir");
let prompt =
system_prompt_flat_text(&system_prompt_for_mode_with_context(tmp.path(), None));
assert!(!prompt.contains("# Session relay"));
assert!(!prompt.contains("## Verification"));
}
#[test]
fn session_goal_stays_volatile_while_compact_template_is_lazy() {
let tmp = tempdir().expect("tempdir");
let prompt =
system_prompt_flat_text(&system_prompt_for_mode_with_context_skills_and_session(
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: false,
locale_tag: "en",
translation_enabled: false,
model_id: "codewhale",
context_window_override: None,
verbosity: None,
skills_scan_codewhale_only: false,
plugin_registry: None,
mode: crate::tui::app::AppMode::Agent,
},
));
let goal_pos = prompt.find("<session_goal>").expect("goal block");
assert!(prompt.contains("Fix transcript corruption"));
assert!(goal_pos > 0);
assert!(!prompt.contains("# Session relay"));
assert!(!prompt.contains("src/lib.rs"));
}
#[test]
fn empty_session_goal_is_not_injected() {
let tmp = tempdir().expect("tempdir");
let prompt =
system_prompt_flat_text(&system_prompt_for_mode_with_context_skills_and_session(
tmp.path(),
None,
None,
None,
PromptSessionContext {
user_memory_block: None,
goal_objective: Some(" "),
project_context_pack_enabled: false,
locale_tag: "en",
translation_enabled: false,
model_id: "codewhale",
context_window_override: None,
verbosity: None,
skills_scan_codewhale_only: false,
plugin_registry: None,
mode: crate::tui::app::AppMode::Agent,
},
));
assert!(!prompt.contains("<session_goal>"));
assert!(!prompt.contains("## Current Goal"));
}
#[test]
fn universal_prompt_leaves_tool_selection_to_the_catalog() {
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
assert!(!prompt.contains("Tool Selection Guide"));
for forbidden in [
"`File`",
"`Git`",
"`Run`",
"`Bash`",
"read_file",
"git_status",
"run_tests",
"exec_shell",
"When NOT to use certain tools",
"Don't reach for",
] {
assert!(!HEADLESS_BASE_PROMPT.contains(forbidden));
}
}
#[test]
fn language_segment_present_outside_reduced_constitution() {
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
assert!(
!BASE_PROMPT.contains("## Language"),
"0.9.0 constitution.md should stay reduced; language belongs in its own segment"
);
assert!(
LANGUAGE_PROMPT.contains("## Language") && prompt.contains("## Language"),
"default static prompt must still include the language segment"
);
assert!(
LANGUAGE_PROMPT.contains("latest user message")
&& LANGUAGE_PROMPT.contains("fallback, not an override")
&& LANGUAGE_PROMPT.contains("localized READMEs")
&& LANGUAGE_PROMPT.contains("Use the `lang` field only when")
&& LANGUAGE_PROMPT.contains("constitution and other system law stay English"),
"language segment must keep the mirror contract while staying short (#4784)"
);
assert!(
LANGUAGE_PROMPT.contains("reasoning_content")
&& prompt.contains("reasoning_content")
&& LOCALE_PREAMBLE_ZH_HANS.contains("reasoning_content")
&& LOCALE_CLOSER_ZH_HANS.contains("reasoning_content"),
"language segment and locale bookends must keep the reasoning_content anchor"
);
}
#[test]
fn output_formatting_segment_present_outside_reduced_constitution() {
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
assert!(
!BASE_PROMPT.contains("## Output Formatting"),
"0.9.0 constitution.md should stay reduced; output formatting belongs in its own segment"
);
assert!(OUTPUT_PROMPT.contains("## Output Formatting"));
assert!(prompt.contains("## Output Formatting"));
assert!(prompt.contains("terminal, not a browser"));
assert!(prompt.contains("Markdown tables almost never render correctly"));
}
#[test]
fn runtime_prompt_assembly_preserves_split_static_layers() {
let tmp = tempdir().expect("tempdir");
let prompt =
system_prompt_flat_text(&system_prompt_for_mode_with_context_skills_and_session(
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: "glm-5.2",
context_window_override: Some(1_000_000),
verbosity: None,
skills_scan_codewhale_only: false,
plugin_registry: None,
mode: crate::tui::app::AppMode::Agent,
},
));
assert!(prompt.contains("## Codewhale"));
assert!(prompt.contains("## Language"));
assert!(prompt.contains("## Output Formatting"));
assert!(prompt.contains("Use the `lang` field only when"));
}
#[test]
fn locale_bookends_resist_english_context_drift() {
assert!(
LOCALE_PREAMBLE_ZH_HANS.contains("reasoning_content")
&& LOCALE_CLOSER_ZH_HANS.contains("reasoning_content"),
"locale bookends must keep the reasoning_content anchor"
);
assert!(
LOCALE_CLOSER_ZH_HANS.contains("英文代码")
&& LOCALE_CLOSER_ZH_HANS.contains("用户的语言决定"),
"closing locale bookend must explicitly resist English-context drift"
);
assert!(
LOCALE_PREAMBLE_ZH_HANS.contains("代码、文件路径、工具名称"),
"opening locale bookend must keep code/tool tokens untranslated"
);
}
#[test]
fn english_base_prompt_avoids_native_script_language_priming() {
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
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 runtime bookends"
);
}
#[test]
fn legacy_rlm_compatibility_descriptions_remain_available() {
let descriptions = [
RlmTool::alias("rlm_open", "open", None)
.description()
.to_string(),
RlmTool::alias("rlm_eval", "eval", None)
.description()
.to_string(),
RlmTool::alias("rlm_configure", "configure", None)
.description()
.to_string(),
RlmTool::alias("rlm_close", "close", None)
.description()
.to_string(),
HandleReadTool.description().to_string(),
]
.join("\n");
let rlm_count = descriptions.to_lowercase().matches("rlm").count();
assert!(
rlm_count >= 5,
"RLM tool descriptions present: expected >= 5 mentions of 'rlm', got {rlm_count}"
);
assert!(!HEADLESS_BASE_PROMPT.contains("`rlm`"));
}
#[test]
fn project_instructions_outrank_memory_in_whose_word_wins() {
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
let project_at = prompt
.find("3. Project law and instructions")
.expect("Whose word wins must rank project instructions");
let memory_at = prompt
.find("5. Memory and previous-session handoffs.")
.expect("Whose word wins must rank memory below project instructions");
assert!(
project_at < memory_at,
"project instructions must outrank memory so embedder-injected \
instructions are not treated as mere memory preferences"
);
}
#[test]
fn workspace_orientation_guidance_present() {
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
assert!(prompt.contains("Project law and instructions"));
assert!(
prompt.contains("the nearest in\nscope winning over the broader")
|| prompt.contains("the nearest in scope winning over the broader"),
"Whose word wins must keep the nearest-scope-wins rule for project instructions"
);
}
#[test]
fn prompt_documents_fork_context_prefix_cache_contract() {
let source = include_str!("tools/subagent/mod.rs");
assert!(source.contains("fork_context"));
assert!(!HEADLESS_BASE_PROMPT.contains("fork_context"));
}
#[test]
fn prompt_documents_explicit_subagent_model_strength() {
let source = include_str!("tools/subagent/mod.rs");
assert!(source.contains("model_strength"));
assert!(!HEADLESS_BASE_PROMPT.contains("model_strength"));
}
#[test]
fn prompt_documents_structured_subagent_briefs() {
assert!(!HEADLESS_BASE_PROMPT.contains("Subagent Brief"));
for heading in [
"### SUMMARY",
"### EVIDENCE",
"### CHANGES",
"### RISKS",
"### BLOCKERS",
] {
assert!(text::SUBAGENT_OUTPUT_FORMAT.contains(heading));
}
}
#[test]
fn universal_prompt_does_not_invent_orchestration_limits() {
assert!(!HEADLESS_BASE_PROMPT.contains("3-5 tool calls"));
assert!(!HEADLESS_BASE_PROMPT.contains("No fan-out without a fan-in owner"));
}
#[test]
fn universal_prompt_does_not_teach_optional_workflow_recipes() {
for recipe in [
"Workflow",
"responseSchema",
"request_user_input",
".workflow.js",
] {
assert!(!HEADLESS_BASE_PROMPT.contains(recipe));
}
}
#[test]
fn universal_prompt_does_not_expose_control_plane_ceremony() {
for internal in [
"sub-agent",
"completion sentinels",
"<codewhale:subagent.done>",
"dispatch, join",
"busy-waiting",
] {
assert!(!HEADLESS_BASE_PROMPT.contains(internal));
}
}
#[test]
fn preamble_carries_tone_and_ownership_guidance() {
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
assert!(prompt.contains("The A is already yours"));
assert!(prompt.contains("Your competence is a settled fact"));
assert!(prompt.contains("Take the work seriously. Don't take"));
assert!(prompt.contains("Let the work speak"));
}
use crate::test_support::{EnvVarGuard, assert_byte_identical};
#[test]
fn compose_prompt_is_byte_stable_across_calls() {
let a = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
let b = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
assert_byte_identical("compose_prompt(Personality::Calm)", &a, &b);
}
#[test]
fn system_prompt_for_mode_with_context_is_byte_stable_for_unchanged_workspace() {
let _env_guard = crate::test_support::lock_test_env();
let workspace_tmp = tempdir().expect("workspace tempdir");
let home_tmp = tempdir().expect("home tempdir");
let _home = EnvVarGuard::set("HOME", home_tmp.path().as_os_str());
let _userprofile = EnvVarGuard::set("USERPROFILE", home_tmp.path().as_os_str());
let _skills_dir = EnvVarGuard::remove("DEEPSEEK_SKILLS_DIR");
let workspace = workspace_tmp.path();
let a = system_prompt_flat_text(&system_prompt_for_mode_with_context(workspace, None));
let b = system_prompt_flat_text(&system_prompt_for_mode_with_context(workspace, None));
assert_byte_identical(
"system_prompt_for_mode_with_context() on empty workspace",
&a,
&b,
);
}
#[test]
fn system_prompt_ignores_working_set_summary_argument() {
let _env_guard = crate::test_support::lock_test_env();
let tmp = tempdir().expect("tempdir");
let home_tmp = tempdir().expect("home tempdir");
let _home = EnvVarGuard::set("HOME", home_tmp.path().as_os_str());
let _userprofile = EnvVarGuard::set("USERPROFILE", home_tmp.path().as_os_str());
let _skills_dir = EnvVarGuard::remove("DEEPSEEK_SKILLS_DIR");
let workspace = tmp.path();
let summary = "## Repo Working Set\nWorkspace: /tmp/x\n";
let a = system_prompt_flat_text(&system_prompt_for_mode_with_context(
workspace,
Some(summary),
));
let b = system_prompt_flat_text(&system_prompt_for_mode_with_context(
workspace,
Some(summary),
));
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 _env_guard = crate::test_support::lock_test_env();
let tmp = tempdir().expect("tempdir");
let home_tmp = tempdir().expect("home tempdir");
let _home = EnvVarGuard::set("HOME", home_tmp.path().as_os_str());
let _userprofile = EnvVarGuard::set("USERPROFILE", home_tmp.path().as_os_str());
let _skills_dir = EnvVarGuard::remove("DEEPSEEK_SKILLS_DIR");
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 = system_prompt_flat_text(&system_prompt_for_mode_with_context(workspace, None));
let b = system_prompt_flat_text(&system_prompt_for_mode_with_context(workspace, None));
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 = system_prompt_flat_text(&system_prompt_for_mode_with_context(
workspace,
Some(summary),
));
let execution_pos = prompt
.find("## Core Execution")
.expect("Core Execution section present in Agent mode");
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!(
execution_pos < handoff_pos,
"## Core Execution must precede the relay block"
);
assert!(!prompt.contains("# Session relay"));
}
#[test]
fn render_instructions_block_returns_none_for_empty_input() {
let empty: &[super::InstructionSource] = &[];
assert!(super::render_instructions_block(empty).is_none());
}
#[test]
fn system_prompt_prefix_never_leaks_private_content() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let prompt = match system_prompt_for_mode_with_context(workspace, None) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(blocks) => blocks
.iter()
.map(|block| block.text.as_str())
.collect::<Vec<_>>()
.join("\n"),
};
let offending: Vec<&str> = prompt
.lines()
.filter(|line| {
line.contains("/Users/") || line.contains("/home/") || line.contains("C:\\")
})
.collect();
assert!(
offending.is_empty(),
"system prompt must not contain absolute user paths, found: {offending:?}"
);
assert!(
!prompt.contains("sk-") && !prompt.contains("api_key") && !prompt.contains("API_KEY"),
"system prompt must not contain API key material"
);
assert!(
!prompt.contains(workspace.to_str().unwrap_or("/nonexistent")),
"system prompt must not embed the workspace path"
);
}
#[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().into(), real.clone().into()])
.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.into(), b.into()]).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.into(), real.into()]).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.into()]).expect("non-empty");
assert!(block.contains("[…truncated:"), "truncation marker missing");
assert!(
block.len() < 110 * 1024,
"block should be capped near 100 KiB"
);
}
#[test]
fn render_instructions_block_handles_inline_source() {
let block = super::render_instructions_block(&[super::InstructionSource::Inline {
name: "embedded:test/template".to_string(),
content: "INLINE_MARKER_CONTENT".to_string(),
}])
.expect("non-empty");
assert!(block.contains("INLINE_MARKER_CONTENT"));
assert!(block.contains("source=\"embedded:test/template\""));
let empty_inline = super::InstructionSource::Inline {
name: "empty".to_string(),
content: " ".to_string(),
};
assert!(super::render_instructions_block(&[empty_inline]).is_none());
let big_inline = super::InstructionSource::Inline {
name: "huge".to_string(),
content: "Y".repeat(200 * 1024),
};
let trimmed = super::render_instructions_block(&[big_inline]).expect("non-empty");
assert!(trimmed.contains("[…truncated:"));
let tmp = tempdir().expect("tempdir");
let file_path = tmp.path().join("file-first.md");
std::fs::write(&file_path, "FILE_MARKER").unwrap();
let mixed = super::render_instructions_block(&[
file_path.into(),
super::InstructionSource::Inline {
name: "inline-second".to_string(),
content: "INLINE_MARKER".to_string(),
},
])
.expect("non-empty");
let file_pos = mixed.find("FILE_MARKER").expect("file rendered");
let inline_pos = mixed.find("INLINE_MARKER").expect("inline rendered");
assert!(file_pos < inline_pos, "声明顺序必须保留(File then Inline)");
}
#[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 extra_source: super::InstructionSource = extra.clone().into();
let prompt =
system_prompt_flat_text(&super::system_prompt_for_mode_with_context_and_skills(
workspace,
None,
None,
Some(std::slice::from_ref(&extra_source)),
None,
));
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"
);
}
#[test]
fn verbosity_concise_appends_discipline_block() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let prompt = system_prompt_flat_text(
&super::system_prompt_for_mode_with_context_skills_session_and_approval(
workspace,
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",
context_window_override: None,
verbosity: Some(" Concise "),
skills_scan_codewhale_only: false,
plugin_registry: None,
mode: crate::tui::app::AppMode::Agent,
},
),
);
assert!(
prompt.contains("## Concise Output Discipline"),
"Concise Output Discipline should be appended"
);
}
#[test]
fn default_prompt_does_not_include_calm_personality_overlay() {
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
let calm_text = CALM_PERSONALITY;
let first_calm_line = calm_text.lines().find(|l| !l.is_empty()).unwrap_or("");
assert!(
!prompt.contains(first_calm_line),
"default agent prompt must not include the calm personality overlay"
);
}
#[test]
fn live_prompt_path_returns_world_state_blocks_with_markers() {
let tmp = tempdir().expect("tempdir");
let prompt = system_prompt_for_mode_with_context_skills_session_and_approval(
tmp.path(),
None,
None,
None,
PromptSessionContext {
user_memory_block: Some("## Memory\n- remember the cutover"),
goal_objective: Some("ship WorldState Blocks"),
project_context_pack_enabled: false,
locale_tag: "en",
translation_enabled: false,
model_id: "deepseek-v4-pro",
context_window_override: None,
verbosity: Some("concise"),
skills_scan_codewhale_only: false,
plugin_registry: None,
mode: crate::tui::app::AppMode::Agent,
},
);
let SystemPrompt::Blocks(blocks) = prompt else {
panic!("live prompt assembly must return SystemPrompt::Blocks");
};
assert!(
blocks.len() >= 3,
"constitution + at least one WorldState fragment + authority trailer"
);
assert!(
!blocks[0].text.contains("<!-- cw:ctx:"),
"constitution block must stay marker-free for prefix cache stability"
);
assert!(
blocks[0].text.contains("## Core Execution"),
"constitution retains static core execution guidance"
);
let flat = system_prompt_flat_text(&SystemPrompt::Blocks(blocks.clone()));
assert!(flat.contains(crate::model_context::FragmentId::Workspace.marker()));
assert!(flat.contains(crate::model_context::FragmentId::Route.marker()));
assert!(flat.contains("## Environment"));
assert!(
flat.contains("verbosity: concise") && flat.contains("translation: off"),
"route fragment keeps verbosity/translation but drops the model id"
);
assert!(!flat.contains("model: deepseek-v4-pro"));
assert!(flat.contains("<session_goal>"));
assert!(flat.contains("ship WorldState Blocks"));
assert!(flat.contains("remember the cutover"));
assert!(flat.contains("## Authority Recap"));
assert!(
!flat.contains(crate::model_context::FragmentId::SkillsTools.marker()),
"skills remain in constitution, not a volatile SkillsTools fragment"
);
}
#[test]
fn live_prompt_world_state_diff_retains_unchanged_fragments() {
let tmp = tempdir().expect("tempdir");
let session = PromptSessionContext {
user_memory_block: None,
goal_objective: None,
project_context_pack_enabled: false,
locale_tag: "en",
translation_enabled: false,
model_id: "codewhale",
context_window_override: None,
verbosity: None,
skills_scan_codewhale_only: false,
plugin_registry: None,
mode: crate::tui::app::AppMode::Agent,
};
let first = system_prompt_for_mode_with_context_skills_session_and_approval(
tmp.path(),
None,
None,
None,
session.clone(),
);
let second = system_prompt_for_mode_with_context_skills_session_and_approval(
tmp.path(),
None,
None,
None,
PromptSessionContext {
goal_objective: Some("only goal changed"),
..session
},
);
let extract_world = |prompt: &SystemPrompt| -> crate::model_context::WorldState {
let SystemPrompt::Blocks(blocks) = prompt else {
panic!("expected Blocks");
};
let mut state = crate::model_context::WorldState::new();
for block in blocks.iter().skip(1) {
for id in crate::model_context::FragmentId::all() {
let marker = id.marker();
if let Some(rest) = block.text.strip_prefix(marker) {
let body = rest.trim_start_matches('\n');
state.upsert(crate::model_context::ModelContextFragment::new(
*id,
id.role(),
body,
));
}
}
}
state
};
let previous = extract_world(&first);
let next = extract_world(&second);
let diff = next.render_diff(Some(&previous));
assert!(
diff.retained
.iter()
.any(|marker| marker == crate::model_context::FragmentId::Route.marker()),
"unchanged route fragment must be retained: {diff:?}"
);
assert!(
diff.updated
.iter()
.any(|fragment| fragment.id == crate::model_context::FragmentId::Workspace),
"goal change must update workspace fragment: {diff:?}"
);
}
#[test]
fn default_prompt_stays_under_2953_static_baseline() {
const ISSUE_2953_BASELINE_CHARS: usize = 30_461;
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
assert!(
prompt.chars().count() < ISSUE_2953_BASELINE_CHARS,
"default static prompt should stay below the #2953 baseline"
);
}
}
#[test]
fn core_execution_profile_is_runtime_only() {
for required in [
"repository instructions",
"inspect the narrow owner",
"verify it",
"Report changed files",
] {
assert!(CORE_EXECUTION_PROFILE_PROMPT.contains(required));
}
for forbidden in [
"footer",
"color",
"hotbar",
"panel",
"Fleet",
"Workflow",
"OpenHands",
] {
assert!(!CORE_EXECUTION_PROFILE_PROMPT.contains(forbidden));
}
}