Add edge case tests for history editing feature (issue 114)

- Add test for editing a message and removing all content (empty content)
- Add test for multiple consecutive edits to the same message (changes stack)
- Add test for editing messages with special characters
- Add test for very long message content
- Add test for editing assistant messages with tool_calls
- Add test for dropping all messages (empty history result)
- Add test for unicode characters in message content
- Add test for whitespace variations in rebase file
- Add test for comments and empty lines mixed with commands
- Add test for special characters and unicode in rebase content display
- Fix apply_rebase to preserve original content when only role is changed
- Add content_edited flag to RebaseLine to track explicit edits
This commit is contained in:
2026-05-30 19:49:05 -05:00
parent 51c911df8d
commit 7fe1656c7d
5 changed files with 651 additions and 2 deletions
+1
View File
@@ -35,6 +35,7 @@ struct RebaseLine {
size_t index;
std::string role;
std::string content; // For edit commands, stores the new content
bool content_edited = false; // True if content was explicitly edited (even if empty)
};
DRH_DEFINE_TYPE_MIXED(RebaseLine, (command, index, role), (content))
+3 -2
View File
@@ -253,8 +253,8 @@ std::expected<std::vector<Message>, std::string> RebaseEngine::apply_rebase(
// Create a copy and update role and content
Message edited = history[i];
edited.role = cmd->role;
// Only update content if it was provided (non-empty means it was edited)
if (!cmd->content.empty()) {
// Update content only if it was explicitly edited
if (cmd->content_edited) {
edited.content = cmd->content;
}
result.push_back(std::move(edited));
@@ -326,6 +326,7 @@ std::expected<std::vector<Message>, std::string> HistoryEditor::edit_history_int
for (auto& line : lines) {
if (line.index == edit_idx && line.command == RebaseCommand::Edit) {
line.content = edit_result.value();
line.content_edited = true; // Mark that content was explicitly edited
break;
}
}
+534
View File
@@ -922,6 +922,533 @@ EDITEOF
echo "PASS: $test_name"
}
# Test 14: Editing a message and removing all content (empty content after edit)
test_edit_remove_all_content() {
reset_editor_state
local test_name="edit_remove_all_content"
# Create a chat file with two messages
create_chat_file chat.json user "hello" assistant "hi there"
# Stage the rebase editor output (edit message 1)
cat > .editor_input_0 << 'EDITEOF'
# History Rebase Editor
#
# Commands:
# pick - keep the message unchanged
# edit - modify the message content and/or role
# drop - remove the message from history
#
# Each line format: <command> <index> [<role>] <display_content>
# - index: 0-based message index
# - role: [user], [assistant], or [tool] (editable)
# - display_content: first line of message (display only, not editable here)
#
# Lines starting with # are comments and will be ignored.
# Empty lines are treated as drops.
#
-- >8 --
pick 0 [user] hello
edit 1 [assistant] hi there
EDITEOF
# Stage the edit prompt response (empty content)
cat > .editor_input_1 << 'EDITEOF'
# Previous message:
[0] user: hello
Edit message 1 (role: assistant) below:
-- >8 --
EDITEOF
# Run drharness with -b flag
set +e
VISUAL="$MOCK_EDITOR" run_drharness -b -F chat.json
EXIT_CODE=$?
OUTPUT=$(get_output)
set -e
if [ ${EXIT_CODE} -ne 0 ]; then
echo "FAIL: $test_name - expected exit code 0, got $EXIT_CODE"
echo "Output: $OUTPUT"
return 1
fi
# Verify history still has 2 messages
local len=$(history_length chat.json)
if [ "$len" != "2" ]; then
echo "FAIL: $test_name - expected 2 messages, got $len"
return 1
fi
# Verify message 1 content is now empty
local msg1_content=$(dr_result chat.json 1)
if [ "$msg1_content" != "" ]; then
echo "FAIL: $test_name - message 1 content should be empty, got: $msg1_content"
return 1
fi
echo "PASS: $test_name"
}
# Test 15: Multiple consecutive edits to the same message (changes stack correctly)
test_multiple_consecutive_edits_same_message() {
reset_editor_state
local test_name="multiple_consecutive_edits_same_message"
# Create a chat file with two messages
create_chat_file chat.json user "hello" assistant "original"
# First session: Edit message 1 to change content from "original" to "first edit"
# Staged inputs for first drharness invocation:
# Invocation 0: rebase editor
# Invocation 1: content editor for message 1
cat > .editor_input_0 << 'EDITEOF'
# History Rebase Editor
#
# Commands:
# pick - keep the message unchanged
# edit - modify the message content and/or role
# drop - remove the message from history
#
# Each line format: <command> <index> [<role>] <display_content>
# - index: 0-based message index
# - role: [user], [assistant], or [tool] (editable)
# - display_content: first line of message (display only, not editable here)
#
# Lines starting with # are comments and will be ignored.
# Empty lines are treated as drops.
#
-- >8 --
pick 0 [user] hello
edit 1 [assistant] original
EDITEOF
cat > .editor_input_1 << 'EDITEOF'
# Previous message:
[0] user: hello
Edit message 1 (role: assistant) below:
-- >8 --
first edit
EDITEOF
# Staged inputs for second drharness invocation:
# Invocation 2: rebase editor (second session)
# Invocation 3: content editor for message 1 (second session)
cat > .editor_input_2 << 'EDITEOF'
# History Rebase Editor
#
# Commands:
# pick - keep the message unchanged
# edit - modify the message content and/or role
# drop - remove the message from history
#
# Each line format: <command> <index> [<role>] <display_content>
# - index: 0-based message index
# - role: [user], [assistant], or [tool] (editable)
# - display_content: first line of message (display only, not editable here)
#
# Lines starting with # are comments and will be ignored.
# Empty lines are treated as drops.
#
-- >8 --
pick 0 [user] hello
edit 1 [assistant] first edit
EDITEOF
cat > .editor_input_3 << 'EDITEOF'
# Previous message:
[0] user: hello
Edit message 1 (role: assistant) below:
-- >8 --
second edit
EDITEOF
# Run first edit session
set +e
VISUAL="$MOCK_EDITOR" run_drharness -b -F chat.json
EXIT_CODE=$?
OUTPUT=$(get_output)
set -e
if [ ${EXIT_CODE} -ne 0 ]; then
echo "FAIL: $test_name - first edit expected exit code 0, got $EXIT_CODE"
echo "Output: $OUTPUT"
return 1
fi
# Verify first edit was applied
local msg1_content=$(dr_result chat.json 1)
if [ "$msg1_content" != "first edit" ]; then
echo "FAIL: $test_name - after first edit, expected 'first edit', got: $msg1_content"
return 1
fi
# Run second edit session (counter continues from 2)
set +e
VISUAL="$MOCK_EDITOR" run_drharness -b -F chat.json
EXIT_CODE=$?
OUTPUT=$(get_output)
set -e
if [ ${EXIT_CODE} -ne 0 ]; then
echo "FAIL: $test_name - second edit expected exit code 0, got $EXIT_CODE"
echo "Output: $OUTPUT"
return 1
fi
# Verify second edit was applied (changes stack correctly)
msg1_content=$(dr_result chat.json 1)
if [ "$msg1_content" != "second edit" ]; then
echo "FAIL: $test_name - after second edit, expected 'second edit', got: $msg1_content"
return 1
fi
echo "PASS: $test_name"
}
# Test 16: Editing messages with special characters in content
test_edit_special_characters() {
reset_editor_state
local test_name="edit_special_characters"
# Create a chat file with special characters (quotes, dollar sign)
create_chat_file chat.json user 'hello "quotes" and $dollar' assistant 'backslash\\here'
# Stage the rebase editor output (edit message 1)
cat > .editor_input_0 << 'EDITEOF'
# History Rebase Editor
#
# Commands:
# pick - keep the message unchanged
# edit - modify the message content and/or role
# drop - remove the message from history
#
# Each line format: <command> <index> [<role>] <display_content>
# - index: 0-based message index
# - role: [user], [assistant], or [tool] (editable)
# - display_content: first line of message (display only, not editable here)
#
# Lines starting with # are comments and will be ignored.
# Empty lines are treated as drops.
#
-- >8 --
pick 0 [user] hello "quotes" and $dollar
edit 1 [assistant] backslash\here
EDITEOF
# Stage the edit prompt response (with special characters)
cat > .editor_input_1 << 'EDITEOF'
# Previous message:
[0] user: hello "quotes" and $dollar
Edit message 1 (role: assistant) below:
-- >8 --
edited with "quotes" and $dollar signs
EDITEOF
# Run drharness with -b flag
set +e
VISUAL="$MOCK_EDITOR" run_drharness -b -F chat.json
EXIT_CODE=$?
OUTPUT=$(get_output)
set -e
if [ ${EXIT_CODE} -ne 0 ]; then
echo "FAIL: $test_name - expected exit code 0, got $EXIT_CODE"
echo "Output: $OUTPUT"
return 1
fi
# Verify history still has 2 messages
local len=$(history_length chat.json)
if [ "$len" != "2" ]; then
echo "FAIL: $test_name - expected 2 messages, got $len"
return 1
fi
# Verify message 1 content has the special characters
local msg1_content=$(dr_result chat.json 1)
if [ "$msg1_content" != 'edited with "quotes" and $dollar signs' ]; then
echo "FAIL: $test_name - message 1 content mismatch, got: $msg1_content"
return 1
fi
echo "PASS: $test_name"
}
# Test 17: Very long message content in rebase editor
test_very_long_content() {
reset_editor_state
local test_name="very_long_content"
# Create a very long message (500 characters)
local long_content=$(printf 'x%.0s' {1..500})
create_chat_file chat.json user "short" assistant "$long_content"
# Stage the rebase editor output (pick all - just verify it handles long content)
cat > .editor_input_0 << EDITEOF
# History Rebase Editor
#
# Commands:
# pick - keep the message unchanged
# edit - modify the message content and/or role
# drop - remove the message from history
#
# Each line format: <command> <index> [<role>] <display_content>
# - index: 0-based message index
# - role: [user], [assistant], or [tool] (editable)
# - display_content: first line of message (display only, not editable here)
#
# Lines starting with # are comments and will be ignored.
# Empty lines are treated as drops.
#
-- >8 --
pick 0 [user] short
pick 1 [assistant] $long_content
EDITEOF
# Run drharness with -b flag
set +e
VISUAL="$MOCK_EDITOR" run_drharness -b -F chat.json
EXIT_CODE=$?
OUTPUT=$(get_output)
set -e
if [ ${EXIT_CODE} -ne 0 ]; then
echo "FAIL: $test_name - expected exit code 0, got $EXIT_CODE"
echo "Output: $OUTPUT"
return 1
fi
# Verify history still has 2 messages
local len=$(history_length chat.json)
if [ "$len" != "2" ]; then
echo "FAIL: $test_name - expected 2 messages, got $len"
return 1
fi
echo "PASS: $test_name"
}
# Test 18: Editing assistant messages that contain tool_calls
test_edit_assistant_with_tool_calls() {
reset_editor_state
local test_name="edit_assistant_with_tool_calls"
# Create a chat file with an assistant message containing tool_calls
cat > chat.json << 'CHATJSON'
{
"history": [
{"role":"user","content":"hello"},
{"role":"assistant","content":"calling tool","tool_calls":[{"id":"call_1","type":"function","function":{"name":"test","arguments":"{}"}}]},
{"role":"tool","content":"result","tool_call_id":"call_1"}
],
"tools": []
}
CHATJSON
# Stage the rebase editor output (edit message 1)
cat > .editor_input_0 << 'EDITEOF'
# History Rebase Editor
#
# Commands:
# pick - keep the message unchanged
# edit - modify the message content and/or role
# drop - remove the message from history
#
# Each line format: <command> <index> [<role>] <display_content>
# - index: 0-based message index
# - role: [user], [assistant], or [tool] (editable)
# - display_content: first line of message (display only, not editable here)
#
# Lines starting with # are comments and will be ignored.
# Empty lines are treated as drops.
#
-- >8 --
pick 0 [user] hello
edit 1 [assistant] [tool_call: test]
drop 2 [tool] result
EDITEOF
# Stage the edit prompt response (edit the content)
cat > .editor_input_1 << 'EDITEOF'
# Previous message:
[0] user: hello
Edit message 1 (role: assistant) below:
-- >8 --
edited tool call message
EDITEOF
# Run drharness with -b flag
set +e
VISUAL="$MOCK_EDITOR" run_drharness -b -F chat.json
EXIT_CODE=$?
OUTPUT=$(get_output)
set -e
if [ ${EXIT_CODE} -ne 0 ]; then
echo "FAIL: $test_name - expected exit code 0, got $EXIT_CODE"
echo "Output: $OUTPUT"
return 1
fi
# Verify history has 2 messages (tool result dropped)
local len=$(history_length chat.json)
if [ "$len" != "2" ]; then
echo "FAIL: $test_name - expected 2 messages, got $len"
return 1
fi
# Verify message 1 still has tool_calls preserved
if ! jq -e '.history[1].tool_calls' chat.json > /dev/null 2>&1; then
echo "FAIL: $test_name - tool_calls should be preserved"
return 1
fi
# Verify message 1 content was updated
local msg1_content=$(dr_result chat.json 1)
if [ "$msg1_content" != "edited tool call message" ]; then
echo "FAIL: $test_name - message 1 content should be updated, got: $msg1_content"
return 1
fi
echo "PASS: $test_name"
}
# Test 19: Dropping all messages results in empty history
test_drop_all_messages() {
reset_editor_state
local test_name="drop_all_messages"
# Create a chat file with two messages
create_chat_file chat.json user "hello" assistant "hi there"
# Stage the rebase editor output (drop all)
cat > .editor_input_0 << 'EDITEOF'
# History Rebase Editor
#
# Commands:
# pick - keep the message unchanged
# edit - modify the message content and/or role
# drop - remove the message from history
#
# Each line format: <command> <index> [<role>] <display_content>
# - index: 0-based message index
# - role: [user], [assistant], or [tool] (editable)
# - display_content: first line of message (display only, not editable here)
#
# Lines starting with # are comments and will be ignored.
# Empty lines are treated as drops.
#
-- >8 --
drop 0 [user] hello
drop 1 [assistant] hi there
EDITEOF
# Run drharness with -b flag
set +e
VISUAL="$MOCK_EDITOR" run_drharness -b -F chat.json
EXIT_CODE=$?
OUTPUT=$(get_output)
set -e
if [ ${EXIT_CODE} -ne 0 ]; then
echo "FAIL: $test_name - expected exit code 0, got $EXIT_CODE"
echo "Output: $OUTPUT"
return 1
fi
# Verify history is empty
local len=$(history_length chat.json)
if [ "$len" != "0" ]; then
echo "FAIL: $test_name - expected 0 messages, got $len"
return 1
fi
echo "PASS: $test_name"
}
# Test 20: Unicode characters in message content
test_unicode_characters() {
reset_editor_state
local test_name="unicode_characters"
# Create a chat file with unicode characters
create_chat_file chat.json user "こんにちは" assistant "👋 hello 世界"
# Stage the rebase editor output (pick all)
cat > .editor_input_0 << 'EDITEOF'
# History Rebase Editor
#
# Commands:
# pick - keep the message unchanged
# edit - modify the message content and/or role
# drop - remove the message from history
#
# Each line format: <command> <index> [<role>] <display_content>
# - index: 0-based message index
# - role: [user], [assistant], or [tool] (editable)
# - display_content: first line of message (display only, not editable here)
#
# Lines starting with # are comments and will be ignored.
# Empty lines are treated as drops.
#
-- >8 --
pick 0 [user] こんにちは
pick 1 [assistant] 👋 hello 世界
EDITEOF
# Run drharness with -b flag
set +e
VISUAL="$MOCK_EDITOR" run_drharness -b -F chat.json
EXIT_CODE=$?
OUTPUT=$(get_output)
set -e
if [ ${EXIT_CODE} -ne 0 ]; then
echo "FAIL: $test_name - expected exit code 0, got $EXIT_CODE"
echo "Output: $OUTPUT"
return 1
fi
# Verify history still has 2 messages
local len=$(history_length chat.json)
if [ "$len" != "2" ]; then
echo "FAIL: $test_name - expected 2 messages, got $len"
return 1
fi
# Verify message content preserved unicode
local msg0_content=$(dr_result chat.json 0)
if [ "$msg0_content" != "こんにちは" ]; then
echo "FAIL: $test_name - message 0 unicode not preserved, got: $msg0_content"
return 1
fi
local msg1_content=$(dr_result chat.json 1)
if [ "$msg1_content" != "👋 hello 世界" ]; then
echo "FAIL: $test_name - message 1 unicode not preserved, got: $msg1_content"
return 1
fi
echo "PASS: $test_name"
}
# Run all tests
test_pick_all_messages
test_no_edits_history_unchanged
@@ -937,5 +1464,12 @@ test_invalid_index_error
test_invalid_role_error
test_multiple_edits
test_edit_then_continue
test_edit_remove_all_content
test_multiple_consecutive_edits_same_message
test_edit_special_characters
test_very_long_content
test_edit_assistant_with_tool_calls
test_drop_all_messages
test_unicode_characters
echo "All edit_history integration tests passed"
+65
View File
@@ -210,6 +210,68 @@ test_scissors_line() {
echo "PASS: $test_name"
}
# Test 10: Special characters in content
test_special_characters() {
local test_name="special_characters"
local json='[{"role":"user","content":"hello \"quotes\" and $dollar"},{"role":"assistant","content":"backslash\\here"}]'
OUTPUT=$("${DRHARNESS}" -B "$json")
# Should handle quotes and special characters
if ! echo "$OUTPUT" | grep -q "pick 0 \[user\]"; then
echo "FAIL: $test_name - should handle special characters"
echo "Got: $OUTPUT"
return 1
fi
echo "PASS: $test_name"
}
# Test 11: Unicode characters in content
test_unicode_characters() {
local test_name="unicode_characters"
local json='[{"role":"user","content":"こんにちは"},{"role":"assistant","content":"👋 hello 世界"}]'
OUTPUT=$("${DRHARNESS}" -B "$json")
# Should preserve unicode characters
if ! echo "$OUTPUT" | grep -q "pick 0 \[user\] こんにちは"; then
echo "FAIL: $test_name - should preserve unicode"
echo "Got: $OUTPUT"
return 1
fi
if ! echo "$OUTPUT" | grep -q "pick 1 \[assistant\] 👋 hello 世界"; then
echo "FAIL: $test_name - should preserve unicode emoji and CJK"
echo "Got: $OUTPUT"
return 1
fi
echo "PASS: $test_name"
}
# Test 12: Very long content (truncation test)
test_very_long_content() {
local test_name="very_long_content"
# Create a very long string (1000 characters)
local long_content=$(printf 'x%.0s' {1..1000})
local json="[{\"role\":\"user\",\"content\":\"$long_content\"}]"
OUTPUT=$("${DRHARNESS}" -B "$json")
# Should handle long content without crashing
if ! echo "$OUTPUT" | grep -q "pick 0 \[user\]"; then
echo "FAIL: $test_name - should handle long content"
echo "Got: $OUTPUT"
return 1
fi
echo "PASS: $test_name"
}
# Run all tests
test_basic_messages
test_multiline_content
@@ -220,6 +282,9 @@ test_header_comments
test_multiple_tool_calls
test_message_indices
test_scissors_line
test_special_characters
test_unicode_characters
test_very_long_content
echo ""
echo "All tests completed."
+48
View File
@@ -334,6 +334,52 @@ test_missing_role() {
echo "PASS: $test_name"
}
# Test 15: Pick commands with whitespace variations
test_whitespace_variations() {
local test_name="whitespace_variations"
# Test with multiple spaces between fields
local content="pick 0 [user] hello
edit 1 [assistant] response
drop 2 [tool] result"
OUTPUT=$("${DRHARNESS}" -P 3 "$content")
local count=$(rebase_line_count "$OUTPUT")
if [ "$count" -ne 3 ]; then
echo "FAIL: $test_name - expected 3 lines, got $count"
echo "Got: $OUTPUT"
return 1
fi
echo "PASS: $test_name"
}
# Test 16: Comments and empty lines mixed with commands
test_comments_and_empty_lines_mixed() {
local test_name="comments_and_empty_lines_mixed"
local content="# Header comment
pick 0 [user] hello
# Middle comment
edit 1 [assistant] response
drop 2 [tool] result
# Footer comment"
OUTPUT=$("${DRHARNESS}" -P 3 "$content")
local count=$(rebase_line_count "$OUTPUT")
if [ "$count" -ne 3 ]; then
echo "FAIL: $test_name - expected 3 lines, got $count"
echo "Got: $OUTPUT"
return 1
fi
echo "PASS: $test_name"
}
# Run all tests
test_valid_rebase_all_commands
test_valid_rebase_with_comments
@@ -350,6 +396,8 @@ test_all_valid_roles
test_invalid_command_digit
test_invalid_index
test_missing_role
test_whitespace_variations
test_comments_and_empty_lines_mixed
echo ""
echo "All tests completed."