mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-08 00:54:49 +08:00
feat: b5-doctor-cmd — batch 5 upstream parity
This commit is contained in:
@@ -1188,6 +1188,9 @@ pub enum SlashCommand {
|
|||||||
AddDir {
|
AddDir {
|
||||||
path: Option<String>,
|
path: Option<String>,
|
||||||
},
|
},
|
||||||
|
History {
|
||||||
|
count: Option<String>,
|
||||||
|
},
|
||||||
Unknown(String),
|
Unknown(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1421,6 +1424,9 @@ pub fn validate_slash_command_input(
|
|||||||
"tag" => SlashCommand::Tag { label: remainder },
|
"tag" => SlashCommand::Tag { label: remainder },
|
||||||
"output-style" => SlashCommand::OutputStyle { style: remainder },
|
"output-style" => SlashCommand::OutputStyle { style: remainder },
|
||||||
"add-dir" => SlashCommand::AddDir { path: remainder },
|
"add-dir" => SlashCommand::AddDir { path: remainder },
|
||||||
|
"history" => SlashCommand::History {
|
||||||
|
count: optional_single_arg(command, &args, "[count]")?,
|
||||||
|
},
|
||||||
other => SlashCommand::Unknown(other.to_string()),
|
other => SlashCommand::Unknown(other.to_string()),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -3942,6 +3948,7 @@ pub fn handle_slash_command(
|
|||||||
| SlashCommand::Tag { .. }
|
| SlashCommand::Tag { .. }
|
||||||
| SlashCommand::OutputStyle { .. }
|
| SlashCommand::OutputStyle { .. }
|
||||||
| SlashCommand::AddDir { .. }
|
| SlashCommand::AddDir { .. }
|
||||||
|
| SlashCommand::History { .. }
|
||||||
| SlashCommand::Unknown(_) => None,
|
| SlashCommand::Unknown(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4256,6 +4263,47 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_history_command_without_count() {
|
||||||
|
// given
|
||||||
|
let input = "/history";
|
||||||
|
|
||||||
|
// when
|
||||||
|
let parsed = SlashCommand::parse(input);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_eq!(parsed, Ok(Some(SlashCommand::History { count: None })));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_history_command_with_numeric_count() {
|
||||||
|
// given
|
||||||
|
let input = "/history 25";
|
||||||
|
|
||||||
|
// when
|
||||||
|
let parsed = SlashCommand::parse(input);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_eq!(
|
||||||
|
parsed,
|
||||||
|
Ok(Some(SlashCommand::History {
|
||||||
|
count: Some("25".to_string())
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_history_with_extra_arguments() {
|
||||||
|
// given
|
||||||
|
let input = "/history 25 extra";
|
||||||
|
|
||||||
|
// when
|
||||||
|
let error = parse_error_message(input);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert!(error.contains("Usage: /history [count]"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_unexpected_arguments_for_no_arg_commands() {
|
fn rejects_unexpected_arguments_for_no_arg_commands() {
|
||||||
// given
|
// given
|
||||||
|
|||||||
@@ -2422,6 +2422,15 @@ fn run_resume_command(
|
|||||||
message: Some(render_doctor_report()?.render()),
|
message: Some(render_doctor_report()?.render()),
|
||||||
json: None,
|
json: None,
|
||||||
}),
|
}),
|
||||||
|
SlashCommand::History { count } => {
|
||||||
|
let limit = parse_history_count(count.as_deref()).map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
|
||||||
|
let entries = collect_session_prompt_history(session);
|
||||||
|
Ok(ResumeCommandOutcome {
|
||||||
|
session: session.clone(),
|
||||||
|
message: Some(render_prompt_history_report(&entries, limit)),
|
||||||
|
json: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
SlashCommand::Unknown(name) => Err(format_unknown_slash_command(name).into()),
|
SlashCommand::Unknown(name) => Err(format_unknown_slash_command(name).into()),
|
||||||
SlashCommand::Bughunter { .. }
|
SlashCommand::Bughunter { .. }
|
||||||
| SlashCommand::Commit { .. }
|
| SlashCommand::Commit { .. }
|
||||||
@@ -2515,6 +2524,7 @@ fn run_repl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
editor.push_history(input);
|
editor.push_history(input);
|
||||||
|
cli.record_prompt_history(&trimmed);
|
||||||
cli.run_turn(&trimmed)?;
|
cli.run_turn(&trimmed)?;
|
||||||
}
|
}
|
||||||
input::ReadOutcome::Cancel => {}
|
input::ReadOutcome::Cancel => {}
|
||||||
@@ -2551,6 +2561,13 @@ struct LiveCli {
|
|||||||
system_prompt: Vec<String>,
|
system_prompt: Vec<String>,
|
||||||
runtime: BuiltRuntime,
|
runtime: BuiltRuntime,
|
||||||
session: SessionHandle,
|
session: SessionHandle,
|
||||||
|
prompt_history: Vec<PromptHistoryEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct PromptHistoryEntry {
|
||||||
|
timestamp_ms: u64,
|
||||||
|
text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RuntimePluginState {
|
struct RuntimePluginState {
|
||||||
@@ -3052,6 +3069,7 @@ impl LiveCli {
|
|||||||
system_prompt,
|
system_prompt,
|
||||||
runtime,
|
runtime,
|
||||||
session,
|
session,
|
||||||
|
prompt_history: Vec::new(),
|
||||||
};
|
};
|
||||||
cli.persist_session()?;
|
cli.persist_session()?;
|
||||||
Ok(cli)
|
Ok(cli)
|
||||||
@@ -3346,6 +3364,10 @@ impl LiveCli {
|
|||||||
println!("{}", render_doctor_report()?.render());
|
println!("{}", render_doctor_report()?.render());
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
SlashCommand::History { count } => {
|
||||||
|
self.print_prompt_history(count.as_deref());
|
||||||
|
false
|
||||||
|
}
|
||||||
SlashCommand::Login
|
SlashCommand::Login
|
||||||
| SlashCommand::Logout
|
| SlashCommand::Logout
|
||||||
| SlashCommand::Vim
|
| SlashCommand::Vim
|
||||||
@@ -3420,6 +3442,35 @@ impl LiveCli {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn record_prompt_history(&mut self, prompt: &str) {
|
||||||
|
let timestamp_ms = std::time::SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.ok()
|
||||||
|
.map_or(self.runtime.session().updated_at_ms, |duration| {
|
||||||
|
u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
|
||||||
|
});
|
||||||
|
self.prompt_history.push(PromptHistoryEntry {
|
||||||
|
timestamp_ms,
|
||||||
|
text: prompt.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_prompt_history(&self, count: Option<&str>) {
|
||||||
|
let limit = match parse_history_count(count) {
|
||||||
|
Ok(limit) => limit,
|
||||||
|
Err(message) => {
|
||||||
|
eprintln!("{message}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let entries = if self.prompt_history.is_empty() {
|
||||||
|
collect_session_prompt_history(self.runtime.session())
|
||||||
|
} else {
|
||||||
|
self.prompt_history.clone()
|
||||||
|
};
|
||||||
|
println!("{}", render_prompt_history_report(&entries, limit));
|
||||||
|
}
|
||||||
|
|
||||||
fn print_sandbox_status() {
|
fn print_sandbox_status() {
|
||||||
let cwd = env::current_dir().expect("current dir");
|
let cwd = env::current_dir().expect("current dir");
|
||||||
let loader = ConfigLoader::default_for(&cwd);
|
let loader = ConfigLoader::default_for(&cwd);
|
||||||
@@ -4171,9 +4222,15 @@ fn render_repl_help() -> String {
|
|||||||
"REPL".to_string(),
|
"REPL".to_string(),
|
||||||
" /exit Quit the REPL".to_string(),
|
" /exit Quit the REPL".to_string(),
|
||||||
" /quit Quit the REPL".to_string(),
|
" /quit Quit the REPL".to_string(),
|
||||||
|
" Up/Down Navigate prompt history".to_string(),
|
||||||
|
" Ctrl-R Reverse-search prompt history".to_string(),
|
||||||
|
" Tab Complete commands, modes, and recent sessions".to_string(),
|
||||||
|
" Ctrl-C Clear input (or exit on empty prompt)".to_string(),
|
||||||
|
" Shift+Enter/Ctrl+J Insert a newline".to_string(),
|
||||||
" Auto-save .claw/sessions/<session-id>.jsonl".to_string(),
|
" Auto-save .claw/sessions/<session-id>.jsonl".to_string(),
|
||||||
" Resume latest /resume latest".to_string(),
|
" Resume latest /resume latest".to_string(),
|
||||||
" Browse sessions /session list".to_string(),
|
" Browse sessions /session list".to_string(),
|
||||||
|
" Show prompt history /history [count]".to_string(),
|
||||||
String::new(),
|
String::new(),
|
||||||
render_slash_command_help(),
|
render_slash_command_help(),
|
||||||
]
|
]
|
||||||
@@ -4889,6 +4946,100 @@ fn write_temp_text_file(
|
|||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_HISTORY_LIMIT: usize = 10;
|
||||||
|
|
||||||
|
fn parse_history_count(raw: Option<&str>) -> Result<usize, String> {
|
||||||
|
let Some(raw) = raw else {
|
||||||
|
return Ok(DEFAULT_HISTORY_LIMIT);
|
||||||
|
};
|
||||||
|
let parsed: usize = raw
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| format!("history: invalid count '{raw}'. Expected a positive integer."))?;
|
||||||
|
if parsed == 0 {
|
||||||
|
return Err("history: count must be greater than 0.".to_string());
|
||||||
|
}
|
||||||
|
Ok(parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_history_timestamp(timestamp_ms: u64) -> String {
|
||||||
|
let secs = timestamp_ms / 1_000;
|
||||||
|
let subsec_ms = timestamp_ms % 1_000;
|
||||||
|
let days_since_epoch = secs / 86_400;
|
||||||
|
let seconds_of_day = secs % 86_400;
|
||||||
|
let hours = seconds_of_day / 3_600;
|
||||||
|
let minutes = (seconds_of_day % 3_600) / 60;
|
||||||
|
let seconds = seconds_of_day % 60;
|
||||||
|
|
||||||
|
let (year, month, day) = civil_from_days(i64::try_from(days_since_epoch).unwrap_or(0));
|
||||||
|
format!(
|
||||||
|
"{year:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}.{subsec_ms:03}Z"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes civil (Gregorian) year/month/day from days since the Unix epoch
|
||||||
|
// (1970-01-01) using Howard Hinnant's `civil_from_days` algorithm.
|
||||||
|
fn civil_from_days(days: i64) -> (i32, u32, u32) {
|
||||||
|
let z = days + 719_468;
|
||||||
|
let era = if z >= 0 { z / 146_097 } else { (z - 146_096) / 146_097 };
|
||||||
|
let doe = (z - era * 146_097) as u64; // [0, 146_096]
|
||||||
|
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365; // [0, 399]
|
||||||
|
let y = yoe as i64 + era * 400;
|
||||||
|
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
|
||||||
|
let mp = (5 * doy + 2) / 153; // [0, 11]
|
||||||
|
let d = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
|
||||||
|
let m = if mp < 10 { mp + 3 } else { mp - 9 }; // [1, 12]
|
||||||
|
let y = y + i64::from(m <= 2);
|
||||||
|
(y as i32, m as u32, d as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_prompt_history_report(entries: &[PromptHistoryEntry], limit: usize) -> String {
|
||||||
|
if entries.is_empty() {
|
||||||
|
return "Prompt history\n Result no prompts recorded yet".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let total = entries.len();
|
||||||
|
let start = total.saturating_sub(limit);
|
||||||
|
let shown = &entries[start..];
|
||||||
|
let mut lines = vec![
|
||||||
|
"Prompt history".to_string(),
|
||||||
|
format!(" Total {total}"),
|
||||||
|
format!(" Showing {} most recent", shown.len()),
|
||||||
|
format!(" Reverse search Ctrl-R in the REPL"),
|
||||||
|
String::new(),
|
||||||
|
];
|
||||||
|
for (offset, entry) in shown.iter().enumerate() {
|
||||||
|
let absolute_index = start + offset + 1;
|
||||||
|
let timestamp = format_history_timestamp(entry.timestamp_ms);
|
||||||
|
let first_line = entry.text.lines().next().unwrap_or("").trim();
|
||||||
|
let display = if first_line.chars().count() > 80 {
|
||||||
|
let truncated: String = first_line.chars().take(77).collect();
|
||||||
|
format!("{truncated}...")
|
||||||
|
} else {
|
||||||
|
first_line.to_string()
|
||||||
|
};
|
||||||
|
lines.push(format!(" {absolute_index:>3}. [{timestamp}] {display}"));
|
||||||
|
}
|
||||||
|
lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_session_prompt_history(session: &Session) -> Vec<PromptHistoryEntry> {
|
||||||
|
let timestamp_ms = session.updated_at_ms;
|
||||||
|
session
|
||||||
|
.messages
|
||||||
|
.iter()
|
||||||
|
.filter(|message| message.role == MessageRole::User)
|
||||||
|
.filter_map(|message| {
|
||||||
|
message.blocks.iter().find_map(|block| match block {
|
||||||
|
ContentBlock::Text { text } => Some(PromptHistoryEntry {
|
||||||
|
timestamp_ms,
|
||||||
|
text: text.clone(),
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn recent_user_context(session: &Session, limit: usize) -> String {
|
fn recent_user_context(session: &Session, limit: usize) -> String {
|
||||||
let requests = session
|
let requests = session
|
||||||
.messages
|
.messages
|
||||||
@@ -6941,24 +7092,26 @@ fn print_help(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
build_runtime_plugin_state_with_loader, build_runtime_with_plugin_state,
|
build_runtime_plugin_state_with_loader, build_runtime_with_plugin_state,
|
||||||
create_managed_session_handle, describe_tool_progress, filter_tool_specs,
|
collect_session_prompt_history, create_managed_session_handle, describe_tool_progress,
|
||||||
format_bughunter_report, format_commit_preflight_report, format_commit_skipped_report,
|
filter_tool_specs, format_bughunter_report, format_commit_preflight_report,
|
||||||
format_compact_report, format_connected_line, format_cost_report,
|
format_commit_skipped_report, format_compact_report, format_connected_line,
|
||||||
format_internal_prompt_progress_line, format_issue_report, format_model_report,
|
format_cost_report, format_history_timestamp, format_internal_prompt_progress_line,
|
||||||
format_model_switch_report, format_permissions_report, format_permissions_switch_report,
|
format_issue_report, format_model_report, format_model_switch_report,
|
||||||
format_pr_report, format_resume_report, format_status_report, format_tool_call_start,
|
format_permissions_report, format_permissions_switch_report, format_pr_report,
|
||||||
format_tool_result, format_ultraplan_report, format_unknown_slash_command,
|
format_resume_report, format_status_report, format_tool_call_start, format_tool_result,
|
||||||
|
format_ultraplan_report, format_unknown_slash_command,
|
||||||
format_unknown_slash_command_message, format_user_visible_api_error,
|
format_unknown_slash_command_message, format_user_visible_api_error,
|
||||||
normalize_permission_mode, parse_args, parse_git_status_branch,
|
normalize_permission_mode, parse_args, parse_git_status_branch,
|
||||||
parse_git_status_metadata_for, parse_git_workspace_summary, permission_policy,
|
parse_git_status_metadata_for, parse_git_workspace_summary, parse_history_count,
|
||||||
print_help_to, push_output_block, render_config_report, render_diff_report,
|
permission_policy, print_help_to, push_output_block, render_config_report,
|
||||||
render_diff_report_for, render_memory_report, render_repl_help, render_resume_usage,
|
render_diff_report, render_diff_report_for, render_memory_report,
|
||||||
resolve_model_alias, resolve_repl_model, resolve_session_reference, response_to_events,
|
render_prompt_history_report, render_repl_help, render_resume_usage, resolve_model_alias,
|
||||||
|
resolve_repl_model, resolve_session_reference, response_to_events,
|
||||||
resume_supported_slash_commands, run_resume_command,
|
resume_supported_slash_commands, run_resume_command,
|
||||||
slash_command_completion_candidates_with_sessions, status_context, validate_no_args,
|
slash_command_completion_candidates_with_sessions, status_context, validate_no_args,
|
||||||
write_mcp_server_fixture, CliAction, CliOutputFormat, CliToolExecutor, GitWorkspaceSummary,
|
write_mcp_server_fixture, CliAction, CliOutputFormat, CliToolExecutor, GitWorkspaceSummary,
|
||||||
InternalPromptProgressEvent, InternalPromptProgressState, LiveCli, LocalHelpTopic,
|
InternalPromptProgressEvent, InternalPromptProgressState, LiveCli, LocalHelpTopic,
|
||||||
SlashCommand, StatusUsage, DEFAULT_MODEL,
|
PromptHistoryEntry, SlashCommand, StatusUsage, DEFAULT_MODEL,
|
||||||
};
|
};
|
||||||
use api::{ApiError, MessageResponse, OutputContentBlock, Usage};
|
use api::{ApiError, MessageResponse, OutputContentBlock, Usage};
|
||||||
use plugins::{
|
use plugins::{
|
||||||
@@ -8811,6 +8964,168 @@ UU conflicted.rs",
|
|||||||
assert!(help.contains("Up/Down"));
|
assert!(help.contains("Up/Down"));
|
||||||
assert!(help.contains("Tab"));
|
assert!(help.contains("Tab"));
|
||||||
assert!(help.contains("Shift+Enter/Ctrl+J"));
|
assert!(help.contains("Shift+Enter/Ctrl+J"));
|
||||||
|
assert!(help.contains("Ctrl-R"));
|
||||||
|
assert!(help.contains("Reverse-search prompt history"));
|
||||||
|
assert!(help.contains("/history [count]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_history_count_defaults_to_ten_when_missing() {
|
||||||
|
// given
|
||||||
|
let raw: Option<&str> = None;
|
||||||
|
|
||||||
|
// when
|
||||||
|
let parsed = parse_history_count(raw);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_eq!(parsed, Ok(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_history_count_accepts_positive_integers() {
|
||||||
|
// given
|
||||||
|
let raw = Some("25");
|
||||||
|
|
||||||
|
// when
|
||||||
|
let parsed = parse_history_count(raw);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_eq!(parsed, Ok(25));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_history_count_rejects_zero() {
|
||||||
|
// given
|
||||||
|
let raw = Some("0");
|
||||||
|
|
||||||
|
// when
|
||||||
|
let parsed = parse_history_count(raw);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert!(parsed.is_err());
|
||||||
|
assert!(parsed.unwrap_err().contains("greater than 0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_history_count_rejects_non_numeric() {
|
||||||
|
// given
|
||||||
|
let raw = Some("abc");
|
||||||
|
|
||||||
|
// when
|
||||||
|
let parsed = parse_history_count(raw);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert!(parsed.is_err());
|
||||||
|
assert!(parsed.unwrap_err().contains("invalid count 'abc'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_history_timestamp_renders_iso8601_utc() {
|
||||||
|
// given
|
||||||
|
// 2023-01-15T12:34:56.789Z -> 1673786096789 ms
|
||||||
|
let timestamp_ms: u64 = 1_673_786_096_789;
|
||||||
|
|
||||||
|
// when
|
||||||
|
let formatted = format_history_timestamp(timestamp_ms);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_eq!(formatted, "2023-01-15T12:34:56.789Z");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_history_timestamp_renders_unix_epoch_origin() {
|
||||||
|
// given
|
||||||
|
let timestamp_ms: u64 = 0;
|
||||||
|
|
||||||
|
// when
|
||||||
|
let formatted = format_history_timestamp(timestamp_ms);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_eq!(formatted, "1970-01-01T00:00:00.000Z");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_prompt_history_report_lists_entries_with_timestamps() {
|
||||||
|
// given
|
||||||
|
let entries = vec![
|
||||||
|
PromptHistoryEntry {
|
||||||
|
timestamp_ms: 1_673_786_096_000,
|
||||||
|
text: "first prompt".to_string(),
|
||||||
|
},
|
||||||
|
PromptHistoryEntry {
|
||||||
|
timestamp_ms: 1_673_786_100_000,
|
||||||
|
text: "second prompt".to_string(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// when
|
||||||
|
let rendered = render_prompt_history_report(&entries, 10);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert!(rendered.contains("Prompt history"));
|
||||||
|
assert!(rendered.contains("Total 2"));
|
||||||
|
assert!(rendered.contains("Showing 2 most recent"));
|
||||||
|
assert!(rendered.contains("Reverse search Ctrl-R in the REPL"));
|
||||||
|
assert!(rendered.contains("2023-01-15T12:34:56.000Z"));
|
||||||
|
assert!(rendered.contains("first prompt"));
|
||||||
|
assert!(rendered.contains("second prompt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_prompt_history_report_truncates_to_limit_from_the_tail() {
|
||||||
|
// given
|
||||||
|
let entries = vec![
|
||||||
|
PromptHistoryEntry {
|
||||||
|
timestamp_ms: 1_000,
|
||||||
|
text: "older".to_string(),
|
||||||
|
},
|
||||||
|
PromptHistoryEntry {
|
||||||
|
timestamp_ms: 2_000,
|
||||||
|
text: "middle".to_string(),
|
||||||
|
},
|
||||||
|
PromptHistoryEntry {
|
||||||
|
timestamp_ms: 3_000,
|
||||||
|
text: "latest".to_string(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// when
|
||||||
|
let rendered = render_prompt_history_report(&entries, 2);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert!(rendered.contains("Total 3"));
|
||||||
|
assert!(rendered.contains("Showing 2 most recent"));
|
||||||
|
assert!(!rendered.contains("older"));
|
||||||
|
assert!(rendered.contains("middle"));
|
||||||
|
assert!(rendered.contains("latest"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_prompt_history_report_handles_empty_history() {
|
||||||
|
// given
|
||||||
|
let entries: Vec<PromptHistoryEntry> = Vec::new();
|
||||||
|
|
||||||
|
// when
|
||||||
|
let rendered = render_prompt_history_report(&entries, 10);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert!(rendered.contains("no prompts recorded yet"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collect_session_prompt_history_extracts_user_text_blocks() {
|
||||||
|
// given
|
||||||
|
let mut session = Session::new();
|
||||||
|
session.push_user_text("hello").unwrap();
|
||||||
|
session.push_user_text("world").unwrap();
|
||||||
|
|
||||||
|
// when
|
||||||
|
let entries = collect_session_prompt_history(&session);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_eq!(entries.len(), 2);
|
||||||
|
assert_eq!(entries[0].text, "hello");
|
||||||
|
assert_eq!(entries[1].text, "world");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user