Merge pull request 'feature/rebase' (#44) from feature/rebase into master
Reviewed-on: https://192.168.8.63/git/darthrubik/drharness/pulls/44
This commit was merged in pull request #44.
This commit is contained in:
@@ -32,6 +32,8 @@ add_library(drharness_lib
|
||||
src/debug_client.cpp
|
||||
src/editor.cpp
|
||||
src/skill.cpp
|
||||
src/rebase_engine.cpp
|
||||
src/utility.cpp
|
||||
src/base64_util.cpp
|
||||
${TOOL_SOURCES}
|
||||
)
|
||||
|
||||
+13
-4
@@ -94,10 +94,19 @@ Exception only if it is API calling code which is hard to test
|
||||
deterministically.
|
||||
|
||||
### Adding new flags to the CLI for testing
|
||||
Adding new flags to the CLI for testing is allowed and ENCOURAGED! For example
|
||||
for the skill stuff, we added a flag -D that just dumps all skill directories
|
||||
found. This is useless to the end user, but lets us test the fact that our
|
||||
skill directory search was working.
|
||||
Adding new flags to the CLI for testing is allowed and ENCOURAGED! These
|
||||
flags serve as **hooks into internal functionality** to enable testing of
|
||||
modules that don't have a natural CLI entry point.
|
||||
|
||||
For example, for the skill stuff, we added a flag -D that just dumps all skill
|
||||
directories found. This is useless to the end user, but lets us test the fact
|
||||
that our skill directory search was working.
|
||||
|
||||
**Test flags should be minimal hooks:**
|
||||
- They should accept input, call the internal function, and output the result
|
||||
- They should NOT contain test logic (assertions, validation, etc.)
|
||||
- All test logic belongs in the Bash test scripts in `tests/`
|
||||
- The flag's only job is to bridge CLI arguments to the internal function
|
||||
|
||||
If you add a new flag for testing you don't need to update the README.md (but
|
||||
updating the -h message is still mandatory).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# drharness
|
||||
|
||||
A C++20 LLM (Open AI Compatible) harness designed for agentic workflows, file
|
||||
A C++23 LLM (Open AI Compatible) harness designed for agentic workflows, file
|
||||
manipulation, and task delegation. It implements a tool-use loop that allows a
|
||||
large language model to dynamically invoke shell commands, edit files, and
|
||||
delegate sub-tasks to other specialized agents. (I wouldn't use the agents
|
||||
@@ -21,7 +21,7 @@ to it)
|
||||
|
||||
## 🛠️ How to Build
|
||||
|
||||
The project requires CMake version 3.16 or newer, C++20 support, `curl`, and the `nlohmann_json` library.
|
||||
The project requires CMake version 3.16 or newer, C++23 support, `curl`, and the `nlohmann_json` library.
|
||||
|
||||
```bash
|
||||
# Configure the build
|
||||
@@ -97,6 +97,9 @@ export EDITOR=nvim # or vscode, nano, vim, emacs, etc.
|
||||
|
||||
*Note: If neither `VISUAL` nor `EDITOR` is set, the harness will refuse to launch the editor and throw an error instead.*
|
||||
|
||||
|
||||
For detailed documentation on history editing, see [docs/history-editing.md](docs/history-editing.md).
|
||||
|
||||
## 🐳 Docker Isolation
|
||||
|
||||
The harness can be compiled and run inside an isolated Alpine Linux container.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
## 📜 History Editing
|
||||
|
||||
If a conversation goes off track, you can surgically edit your conversation history using the interactive history editing mode. This allows you to rewind to a specific point, modify messages, or remove problematic exchanges without starting over.
|
||||
|
||||
### Invoking History Editing
|
||||
|
||||
Use the `-b` or `--edit-history` flag along with `-F` to specify your chat file:
|
||||
|
||||
```bash
|
||||
./build/drharness -F conversation.json -b
|
||||
```
|
||||
|
||||
*Note: The `-F` flag is required because editing requires an existing chat file.*
|
||||
|
||||
### The Rebase Editor Format
|
||||
|
||||
When invoked, the harness opens your configured editor with all messages in your conversation listed in order. Each line follows a git rebase-style format:
|
||||
|
||||
```
|
||||
<command> <index> [<role>] <display_content>
|
||||
```
|
||||
|
||||
**Commands:**
|
||||
- `pick`: Keep the message unchanged
|
||||
- `edit`: Modify the message content and/or role
|
||||
- `drop`: Delete the message (or simply remove the line)
|
||||
|
||||
**Role:** (optional, only for `edit` commands)
|
||||
- `[user]`
|
||||
- `[assistant]`
|
||||
- `[tool]`
|
||||
|
||||
**Display content:** The first line of the message (for reference only)
|
||||
|
||||
Lines starting with `#` are comments and are ignored. Empty lines are treated as drops.
|
||||
|
||||
### Editing Message Content and Roles
|
||||
|
||||
When you mark a message with `edit`, the harness will open your editor again to let you modify the content. The edit prompt shows:
|
||||
- The previous message as context (for reference)
|
||||
- The `>8` separator
|
||||
- The message content you can edit
|
||||
|
||||
You can change the role by modifying the role in the rebase line (e.g., `edit 2 [user]`).
|
||||
|
||||
### Common Workflows
|
||||
|
||||
**Editing a single message:**
|
||||
```
|
||||
pick 0 [user] Hello, can you help me?
|
||||
pick 1 [assistant] Sure! What do you need?
|
||||
edit 2 [assistant] I misunderstood. Let me try again.
|
||||
pick 3 [user] That's better, thanks!
|
||||
```
|
||||
Then, when editing message 2, you'll be prompted to enter the new content.
|
||||
|
||||
**Dropping unwanted messages:**
|
||||
```
|
||||
pick 0 [user] Start the project
|
||||
drop 1 [assistant] Wrong approach
|
||||
pick 2 [user] Let me try a different way
|
||||
```
|
||||
Or simply delete the line:
|
||||
```
|
||||
pick 0 [user] Start the project
|
||||
# pick 1 [assistant] Wrong approach
|
||||
pick 2 [user] Let me try a different way
|
||||
```
|
||||
|
||||
**Modifying message roles:**
|
||||
```
|
||||
pick 0 [user] Write a function
|
||||
edit 1 [user] I meant write a class instead
|
||||
```
|
||||
|
||||
**Continuing a conversation after editing:**
|
||||
After saving and exiting the editor, your changes are persisted to the chat file. The program exits, allowing you to inspect the result. To continue the conversation:
|
||||
```bash
|
||||
./build/drharness -F conversation.json -m "Continue from where we left off"
|
||||
```
|
||||
|
||||
*Note: The history editor uses strict validation. Any syntax error (invalid command, out-of-range index, invalid role) will cause the editor to abort with an aggregated error message. No partial changes are applied.*
|
||||
|
||||
@@ -548,8 +548,12 @@ struct ShellToolParams {
|
||||
DRH_DEFINE_TYPE_OPTIONAL(ShellToolParams, command, timeout_seconds)
|
||||
|
||||
// Safe JSON parsing - returns discarded JSON on parse failure
|
||||
inline nlohmann::json parse_json_safely(const std::string& input) {
|
||||
return nlohmann::json::parse(input, nullptr, false);
|
||||
inline std::expected<nlohmann::json, std::string> parse_json_safely(const std::string& input) {
|
||||
auto j = nlohmann::json::parse(input, nullptr, false);
|
||||
if (j.is_discarded()) {
|
||||
return std::unexpected("error: invalid JSON");
|
||||
}
|
||||
return j;
|
||||
}
|
||||
|
||||
// Safe regex construction - returns error on invalid pattern
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
#include "json_types.hpp"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <expected>
|
||||
|
||||
// Forward declaration
|
||||
struct Message;
|
||||
|
||||
enum class RebaseCommand { Pick, Edit, Drop };
|
||||
|
||||
// JSON serialization for RebaseCommand
|
||||
inline void to_json(nlohmann::json& j, RebaseCommand cmd) {
|
||||
switch (cmd) {
|
||||
case RebaseCommand::Pick: j = "pick"; break;
|
||||
case RebaseCommand::Edit: j = "edit"; break;
|
||||
case RebaseCommand::Drop: j = "drop"; break;
|
||||
}
|
||||
}
|
||||
|
||||
template<>
|
||||
inline std::expected<RebaseCommand, std::string> json_to<RebaseCommand>(const nlohmann::json& j) {
|
||||
if (!j.is_string()) return std::unexpected("expected string for RebaseCommand");
|
||||
std::string s = j.get<std::string>();
|
||||
if (s == "pick") return RebaseCommand::Pick;
|
||||
if (s == "edit") return RebaseCommand::Edit;
|
||||
if (s == "drop") return RebaseCommand::Drop;
|
||||
return std::unexpected("invalid RebaseCommand: '" + s + "'");
|
||||
}
|
||||
|
||||
template<> struct has_json_to<RebaseCommand> : std::true_type {};
|
||||
|
||||
struct RebaseLine {
|
||||
RebaseCommand command;
|
||||
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))
|
||||
|
||||
class RebaseEngine {
|
||||
public:
|
||||
std::expected<std::vector<RebaseLine>, std::string> parse_rebase_file(
|
||||
const std::string& content, size_t history_size) const;
|
||||
|
||||
std::expected<std::vector<Message>, std::string> apply_rebase(
|
||||
const std::vector<RebaseLine>& lines,
|
||||
const std::vector<Message>& history) const;
|
||||
};
|
||||
|
||||
class HistoryEditor {
|
||||
public:
|
||||
// Build the rebase editor content from a message history
|
||||
// Each message is formatted as: pick N [role] first_line_of_content
|
||||
// Tool calls display as: [tool_call: function_name]
|
||||
// Empty messages display as: (empty)
|
||||
std::string build_rebase_content(const std::vector<Message>& history) const;
|
||||
|
||||
// Build the edit prompt for editing a single message
|
||||
// Shows previous message as context, separator, and current content to edit
|
||||
// For message 0, shows "(no previous message)" instead of previous message
|
||||
std::string build_edit_prompt(const std::vector<Message>& history,
|
||||
size_t edit_index,
|
||||
const std::string& role) const;
|
||||
|
||||
// Full interactive edit flow
|
||||
// 1. Build rebase content and spawn editor
|
||||
// 2. Parse and validate rebase commands
|
||||
// 3. For each edit command, build edit prompt and spawn editor
|
||||
// 4. Apply all changes
|
||||
// Returns modified history on success, error message on failure
|
||||
std::expected<std::vector<Message>, std::string> edit_history_interactive(
|
||||
const std::vector<Message>& history) const;
|
||||
|
||||
private:
|
||||
// Extract the first line of content from a message
|
||||
std::string get_first_line(const std::string& content) const;
|
||||
|
||||
// Format display text for a single message
|
||||
std::string get_message_display(const Message& msg) const;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
|
||||
// Trim whitespace from both ends of a string
|
||||
std::string trim(const std::string& s);
|
||||
|
||||
// Generate a random alphanumeric string
|
||||
std::string random_string(size_t length = 8);
|
||||
|
||||
// Generate a random temp file path
|
||||
std::string random_file_name(size_t length = 8);
|
||||
|
||||
// RAII defer helper class for cleanup
|
||||
class Defer {
|
||||
std::function<void()> fn;
|
||||
|
||||
public:
|
||||
Defer(std::function<void()> f) : fn(f) {}
|
||||
~Defer() { fn(); }
|
||||
};
|
||||
|
||||
// Spawn an editor with initial content and return the content after the separator (-- >8 --)
|
||||
std::expected<std::string, std::string> spawn_editor(const std::string& initial_content);
|
||||
|
||||
// Parse an unsigned long from a C-string, returning an error if parsing fails
|
||||
std::expected<unsigned long, std::string> string_toul(std::string const& str);
|
||||
@@ -12,10 +12,11 @@ std::expected<Message, std::string> DebugClient::chat(const std::vector<Message>
|
||||
return std::unexpected("DebugClient failed to read from stdin");
|
||||
}
|
||||
|
||||
auto j = parse_json_safely(input);
|
||||
if (j.is_discarded()) {
|
||||
return std::unexpected("DebugClient invalid JSON input: parse error");
|
||||
auto j_result = parse_json_safely(input);
|
||||
if (!j_result) {
|
||||
return std::unexpected("DebugClient invalid JSON input: " + j_result.error());
|
||||
}
|
||||
auto j = j_result.value();
|
||||
auto result = json_to<ChatResponse>(j);
|
||||
if (!result) {
|
||||
return std::unexpected(std::string("DebugClient invalid JSON input: ") + result.error());
|
||||
|
||||
+43
-125
@@ -1,138 +1,56 @@
|
||||
#include "editor.hpp"
|
||||
#include "ollama_client.hpp"
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include "utility.hpp"
|
||||
#include <sstream>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <random>
|
||||
#include <functional>
|
||||
|
||||
static std::string trim(const std::string& s) {
|
||||
auto start = s.find_first_not_of(" \t\n\r");
|
||||
return (start == std::string::npos) ? "" : s.substr(start);
|
||||
}
|
||||
|
||||
static std::string random_string(size_t length = 8) {
|
||||
static const char chars[] =
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"0123456789";
|
||||
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
static std::uniform_int_distribution<> dis(0, sizeof(chars) - 2);
|
||||
|
||||
std::string s;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
s += chars[dis(gen)];
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
static std::string random_file_name(size_t length = 8)
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
fs::path temp_dir = fs::temp_directory_path();
|
||||
fs::path temp_file = temp_dir / ("tmp_" + random_string(length));
|
||||
|
||||
|
||||
return temp_file.string();
|
||||
}
|
||||
|
||||
class Defer {
|
||||
std::function<void()> fn;
|
||||
|
||||
public:
|
||||
Defer(std::function<void()> f) : fn(f) {}
|
||||
~Defer() { fn(); }
|
||||
};
|
||||
|
||||
|
||||
std::expected<std::string, std::string> run_editor(HarnessState state, bool full_history) {
|
||||
auto file_path = random_file_name();
|
||||
auto cleanup = Defer([&](){
|
||||
std::filesystem::remove(file_path);
|
||||
});
|
||||
std::ostringstream initial_content;
|
||||
|
||||
{
|
||||
std::ofstream file(file_path);
|
||||
if (!file.is_open()) {
|
||||
return std::unexpected("Failed to open temp file for editor.");
|
||||
}
|
||||
|
||||
if (state.history.empty()) {
|
||||
file << "New Conversation.\n\n";
|
||||
file << "This is the start of a new chat. Type your prompt below the separator.\n";
|
||||
file << "The AI will respond after you save and close the editor.\n\n";
|
||||
if (state.history.empty()) {
|
||||
initial_content << "New Conversation.\n\n";
|
||||
initial_content << "This is the start of a new chat. Type your prompt below the separator.\n";
|
||||
initial_content << "The AI will respond after you save and close the editor.\n\n";
|
||||
} else {
|
||||
nlohmann::json j = nlohmann::json(state);
|
||||
if (full_history) {
|
||||
std::ostringstream history_str;
|
||||
print_history(history_str, j, false);
|
||||
initial_content << history_str.str();
|
||||
} else {
|
||||
nlohmann::json j = nlohmann::json(state);
|
||||
if (full_history) {
|
||||
print_history(file, j, false);
|
||||
} else {
|
||||
// Find the last AI response
|
||||
const Message* last_ai_msg = nullptr;
|
||||
for (int i = static_cast<int>(state.history.size()) - 1; i >= 0; --i) {
|
||||
if (state.history[i].role == "assistant") {
|
||||
last_ai_msg = &state.history[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (last_ai_msg) {
|
||||
file << "[" << last_ai_msg->role << "] ";
|
||||
if (!last_ai_msg->reasoning.empty()) {
|
||||
file << "[Thinking: " << last_ai_msg->reasoning << "] ";
|
||||
}
|
||||
file << last_ai_msg->content << "\n\n";
|
||||
} else {
|
||||
// Fallback to last message if no assistant message found
|
||||
const auto& last_msg = state.history.back();
|
||||
file << "[" << last_msg.role << "] " << last_msg.content << "\n\n";
|
||||
// Find the last AI response
|
||||
const Message* last_ai_msg = nullptr;
|
||||
for (int i = static_cast<int>(state.history.size()) - 1; i >= 0; --i) {
|
||||
if (state.history[i].role == "assistant") {
|
||||
last_ai_msg = &state.history[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (last_ai_msg) {
|
||||
initial_content << "[" << last_ai_msg->role << "] ";
|
||||
if (!last_ai_msg->reasoning.empty()) {
|
||||
initial_content << "[Thinking: " << last_ai_msg->reasoning << "] ";
|
||||
}
|
||||
initial_content << last_ai_msg->content << "\n\n";
|
||||
} else {
|
||||
// Fallback to last message if no assistant message found
|
||||
const auto& last_msg = state.history.back();
|
||||
initial_content << "[" << last_msg.role << "] " << last_msg.content << "\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
file << "-- >8 --\n\n";
|
||||
file.close();
|
||||
|
||||
const char* editor_env = getenv("VISUAL");
|
||||
if (!editor_env) editor_env = getenv("EDITOR");
|
||||
if (!editor_env) {
|
||||
return std::unexpected("Neither $VISUAL nor $EDITOR is set.");
|
||||
}
|
||||
|
||||
std::string cmd = std::string(editor_env) + " " + file_path;
|
||||
int res = system(cmd.c_str());
|
||||
if (res != 0) {
|
||||
return std::unexpected("Editor exited with error.");
|
||||
}
|
||||
|
||||
std::ifstream infile(file_path);
|
||||
std::string content((std::istreambuf_iterator<char>(infile)), std::istreambuf_iterator<char>());
|
||||
|
||||
const std::string seperator = "\n-- >8 --\n";
|
||||
size_t pos = content.find(seperator);
|
||||
if (pos == std::string::npos) {
|
||||
return std::unexpected("Scissors not found.");
|
||||
}
|
||||
|
||||
size_t last_pos = std::string::npos;
|
||||
while (pos != std::string::npos) {
|
||||
last_pos = pos;
|
||||
pos = content.find(seperator, pos+1);
|
||||
}
|
||||
|
||||
std::string extracted = content.substr(last_pos + seperator.size());
|
||||
extracted = trim(extracted);
|
||||
|
||||
if (extracted.empty()) {
|
||||
return std::unexpected("Prompt is empty after separator.");
|
||||
}
|
||||
|
||||
return extracted;
|
||||
}
|
||||
|
||||
initial_content << "-- >8 --\n\n";
|
||||
|
||||
auto result = spawn_editor(initial_content.str());
|
||||
if (!result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (result->empty()) {
|
||||
return std::unexpected("Prompt is empty after separator.");
|
||||
}
|
||||
|
||||
return *result;
|
||||
}
|
||||
|
||||
+4
-4
@@ -114,12 +114,12 @@ std::expected<std::string, std::string> Harness::chat(const Message& user_messag
|
||||
if (config_.verbose)
|
||||
std::cerr << prefix << "[tool call] " << tool_name << "(" << args_str << ")\n";
|
||||
|
||||
Content result = "";
|
||||
auto args_json = parse_json_safely(args_str);
|
||||
if (args_json.is_discarded()) {
|
||||
Content result = "";
|
||||
auto args_json_result = parse_json_safely(args_str);
|
||||
if (!args_json_result) {
|
||||
result = "Error: invalid JSON in tool arguments";
|
||||
} else {
|
||||
result = registry_.invoke(tool_name, args_json);
|
||||
result = registry_.invoke(tool_name, args_json_result.value());
|
||||
}
|
||||
|
||||
if (config_.verbose)
|
||||
|
||||
+149
-35
@@ -5,6 +5,8 @@
|
||||
#include "editor.hpp"
|
||||
#include "tool_registry.hpp"
|
||||
#include "agent_profile.hpp"
|
||||
#include "rebase_engine.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "base64_util.hpp"
|
||||
#if !defined(_WIN32)
|
||||
#include "shell_tool.hpp"
|
||||
@@ -76,6 +78,24 @@ static std::string generate_uuid() {
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// Helper function to read and deserialize HarnessState from a file
|
||||
// Returns the parsed HarnessState, or exits on error
|
||||
static HarnessState read_harness_state_or_exit(const char* file_path) {
|
||||
std::ifstream file(file_path);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "Error: Cannot open file " << file_path << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
std::string file_content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
auto data = expect_or_exit(parse_json_safely(file_content));
|
||||
auto state_result = json_to<HarnessState>(data);
|
||||
if (!state_result) {
|
||||
std::cerr << "Error: Failed to parse chat file: " << state_result.error() << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
return state_result.value();
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
OllamaConfig cfg;
|
||||
if (const char* m = std::getenv("OLLAMA_MODEL")) cfg.model = m;
|
||||
@@ -91,6 +111,14 @@ int main(int argc, char** argv) {
|
||||
char const* test_expect_exit_error = nullptr;
|
||||
char const* test_type_required_type = nullptr;
|
||||
char const* test_type_required_json = nullptr;
|
||||
char const* test_rebase_history_size = nullptr;
|
||||
char const* test_rebase_content = nullptr;
|
||||
char const* test_build_rebase_json = nullptr;
|
||||
char const* test_apply_rebase_lines = nullptr;
|
||||
char const* test_apply_rebase_history = nullptr;
|
||||
char const* test_edit_prompt_json = nullptr;
|
||||
char const* test_edit_prompt_index = nullptr;
|
||||
char const* test_edit_prompt_role = nullptr;
|
||||
char const* test_base64_encode = nullptr;
|
||||
char const* test_base64_decode = nullptr;
|
||||
char const* test_mime_type = nullptr;
|
||||
@@ -135,6 +163,7 @@ int main(int argc, char** argv) {
|
||||
char const* print_file = nullptr;
|
||||
std::vector<std::string> tools;
|
||||
bool full_history = false;
|
||||
bool edit_history = false;
|
||||
std::vector<std::string> skill_filters;
|
||||
std::vector<std::string> image_paths;
|
||||
|
||||
@@ -218,15 +247,39 @@ int main(int argc, char** argv) {
|
||||
test_type_required_type = consume_arg();
|
||||
test_type_required_json = consume_arg();
|
||||
}
|
||||
else if (argv[0] == std::string("-z"))
|
||||
{
|
||||
consume_arg();
|
||||
test_rebase_history_size = consume_arg();
|
||||
test_rebase_content = consume_arg();
|
||||
}
|
||||
else if (argv[0] == std::string("-Z"))
|
||||
{
|
||||
consume_arg();
|
||||
test_build_rebase_json = consume_arg();
|
||||
}
|
||||
else if (argv[0] == std::string("-A"))
|
||||
{
|
||||
consume_arg();
|
||||
test_apply_rebase_lines = consume_arg();
|
||||
test_apply_rebase_history = consume_arg();
|
||||
}
|
||||
else if (argv[0] == std::string("-B"))
|
||||
{
|
||||
consume_arg();
|
||||
test_base64_encode = consume_arg();
|
||||
}
|
||||
else if (argv[0] == std::string("-b"))
|
||||
else if (argv[0] == std::string("-K"))
|
||||
{
|
||||
consume_arg();
|
||||
test_base64_decode = consume_arg();
|
||||
}
|
||||
else if (argv[0] == std::string("-W"))
|
||||
{
|
||||
consume_arg();
|
||||
test_edit_prompt_json = consume_arg();
|
||||
test_edit_prompt_index = consume_arg();
|
||||
test_edit_prompt_role = consume_arg();
|
||||
}
|
||||
else if (argv[0] == std::string("-T"))
|
||||
{
|
||||
@@ -262,6 +315,11 @@ int main(int argc, char** argv) {
|
||||
consume_arg();
|
||||
skill_filters.push_back(consume_arg());
|
||||
}
|
||||
else if (argv[0] == std::string("-b") || argv[0] == std::string("--edit-history"))
|
||||
{
|
||||
consume_arg();
|
||||
edit_history = true;
|
||||
}
|
||||
else if (argv[0] == std::string("-i"))
|
||||
{
|
||||
consume_arg();
|
||||
@@ -286,6 +344,8 @@ int main(int argc, char** argv) {
|
||||
<< " -P <file> Read prompt from file\n"
|
||||
<< " -M <model> Ollama model name\n"
|
||||
<< " -f Open editor with full history (requires -F)\n"
|
||||
<< " -b Edit history (requires -F)\n"
|
||||
<< " --edit-history\n"
|
||||
<< " -l List available tools\n"
|
||||
<< " -L List available models on server side\n"
|
||||
<< " -D Debug: print all discovered skill paths and exit\n"
|
||||
@@ -295,8 +355,12 @@ int main(int argc, char** argv) {
|
||||
<< " -E <string> Debug: test expect_or_exit success case and exit\n"
|
||||
<< " -e <string> Debug: test expect_or_exit error case and exit\n"
|
||||
<< " -R <type> <json> Debug: test json_to<Type>() for required fields and exit\n"
|
||||
<< " -z <size> <content> Debug: test rebase parser and exit\n"
|
||||
<< " -Z <json> Debug: test build_rebase_content with JSON history and exit\n"
|
||||
<< " -A <lines> <history> Debug: test apply_rebase with JSON and exit\n"
|
||||
<< " -W <json> <index> <role> Debug: test build_edit_prompt and exit\n"
|
||||
<< " -B <string> Debug: test base64 encode and exit\n"
|
||||
<< " -b <string> Debug: test base64 decode and exit\n"
|
||||
<< " -K <string> Debug: test base64 decode and exit\n"
|
||||
<< " -T <ext> Debug: test MIME type detection and exit\n"
|
||||
<< " -U <ext> <b64> Debug: test data URI generation and exit\n"
|
||||
<< " -I <file> Debug: test image file reading and exit\n"
|
||||
@@ -311,6 +375,44 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate -b / --edit-history flag requirements
|
||||
if (edit_history && !file_to_open)
|
||||
{
|
||||
std::cerr << "Error: -b/--edit-history requires -F <chat_file> to be specified" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Validate -b / --edit-history flag with empty history
|
||||
if (edit_history && file_to_open)
|
||||
{
|
||||
HarnessState state_data = read_harness_state_or_exit(file_to_open);
|
||||
|
||||
if (state_data.history.empty()) {
|
||||
std::cerr << "Error: Chat file has no history, cannot edit" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Run the interactive edit flow
|
||||
HistoryEditor editor;
|
||||
auto edit_result = editor.edit_history_interactive(state_data.history);
|
||||
if (!edit_result) {
|
||||
std::cerr << "Error: " << edit_result.error() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Save modified history to chat file
|
||||
state_data.history = edit_result.value();
|
||||
std::ofstream outfile(file_to_open);
|
||||
if (!outfile.is_open()) {
|
||||
std::cerr << "Error: Cannot write to file " << file_to_open << std::endl;
|
||||
return 1;
|
||||
}
|
||||
outfile << nlohmann::json(state_data);
|
||||
|
||||
// Exit after successful edit
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Helper function to create the client based on the debug flag
|
||||
auto create_client = [&](bool debug) -> std::unique_ptr<Client> {
|
||||
if (debug)
|
||||
@@ -371,11 +473,7 @@ int main(int argc, char** argv) {
|
||||
|
||||
if (test_json_parse) {
|
||||
auto result = parse_json_safely(test_json_parse);
|
||||
if (result.is_discarded()) {
|
||||
std::cout << "discarded" << std::endl;
|
||||
} else {
|
||||
std::cout << "valid" << std::endl;
|
||||
}
|
||||
std::cout << expect_or_exit(result).dump(2) << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -393,23 +491,56 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
|
||||
if (test_type_required_type && test_type_required_json) {
|
||||
auto j = parse_json_safely(test_type_required_json);
|
||||
if (j.is_discarded()) {
|
||||
std::cout << "error: invalid JSON" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto j = expect_or_exit(parse_json_safely(test_type_required_json));
|
||||
std::string type_str(test_type_required_type);
|
||||
auto it = json_dispatch_map.find(type_str);
|
||||
if (it != json_dispatch_map.end()) {
|
||||
it->second(j);
|
||||
} else {
|
||||
std::cout << "error: unknown type " << type_str << std::endl;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (test_rebase_history_size && test_rebase_content) {
|
||||
size_t history_size = expect_or_exit(string_toul(test_rebase_history_size));
|
||||
RebaseEngine engine;
|
||||
auto result = engine.parse_rebase_file(test_rebase_content, history_size);
|
||||
nlohmann::json output = expect_or_exit(result);
|
||||
std::cout << output.dump(2) << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (test_build_rebase_json) {
|
||||
auto j = expect_or_exit(parse_json_safely(test_build_rebase_json));
|
||||
auto history_result = expect_or_exit(json_to<std::vector<Message>>(j));
|
||||
HistoryEditor editor;
|
||||
std::string content = editor.build_rebase_content(history_result);
|
||||
std::cout << content;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (test_apply_rebase_lines && test_apply_rebase_history) {
|
||||
auto lines_j = expect_or_exit(parse_json_safely(test_apply_rebase_lines));
|
||||
auto lines_result = expect_or_exit(json_to<std::vector<RebaseLine>>(lines_j));
|
||||
auto history_j = expect_or_exit(parse_json_safely(test_apply_rebase_history));
|
||||
auto history_result = expect_or_exit(json_to<std::vector<Message>>(history_j));
|
||||
RebaseEngine engine;
|
||||
auto result = engine.apply_rebase(lines_result, history_result);
|
||||
nlohmann::json output = expect_or_exit(result);
|
||||
std::cout << output.dump(2) << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (test_edit_prompt_json && test_edit_prompt_index && test_edit_prompt_role) {
|
||||
auto j = expect_or_exit(parse_json_safely(test_edit_prompt_json));
|
||||
auto history_result = expect_or_exit(json_to<std::vector<Message>>(j));
|
||||
size_t edit_index = expect_or_exit(string_toul(test_edit_prompt_index));
|
||||
const std::string role = test_edit_prompt_role;
|
||||
HistoryEditor editor;
|
||||
std::string prompt = editor.build_edit_prompt(history_result, edit_index, role);
|
||||
std::cout << prompt;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (test_base64_encode) {
|
||||
std::cout << base64_encode(test_base64_encode) << std::endl;
|
||||
return 0;
|
||||
@@ -470,11 +601,7 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
|
||||
std::string file_content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
nlohmann::json data = parse_json_safely(file_content);
|
||||
if (data.is_discarded()) {
|
||||
std::cerr << "Error parsing JSON: invalid JSON" << "\n";
|
||||
return 1;
|
||||
}
|
||||
auto data = expect_or_exit(parse_json_safely(file_content));
|
||||
print_history(std::cout, data);
|
||||
return 0;
|
||||
}
|
||||
@@ -524,20 +651,7 @@ int main(int argc, char** argv) {
|
||||
|
||||
if (!start_new)
|
||||
{
|
||||
std::ifstream old_harness_state(file_to_open);
|
||||
std::string file_content((std::istreambuf_iterator<char>(old_harness_state)), std::istreambuf_iterator<char>());
|
||||
nlohmann::json data = parse_json_safely(file_content);
|
||||
if (data.is_discarded()) {
|
||||
std::cerr << "Failed to parse chat file: invalid JSON" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
auto state_result = json_to<HarnessState>(data);
|
||||
if (!state_result) {
|
||||
std::cerr << "Failed to parse chat file: " << state_result.error() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
HarnessState state_data = state_result.value();
|
||||
|
||||
HarnessState state_data = read_harness_state_or_exit(file_to_open);
|
||||
harness = Harness(create_client(use_debug), master_registery, state_data);
|
||||
}
|
||||
|
||||
|
||||
@@ -137,12 +137,13 @@ std::expected<Message, std::string> OllamaClient::chat(const std::vector<Message
|
||||
}
|
||||
|
||||
//std::cerr << new_packet << std::endl;
|
||||
auto j = parse_json_safely(new_packet.substr(6));
|
||||
if (j.is_discarded())
|
||||
auto j_result = parse_json_safely(new_packet.substr(6));
|
||||
if (!j_result)
|
||||
{
|
||||
std::cerr << "\nDiscarding: " << new_packet << std::endl;
|
||||
continue;
|
||||
}
|
||||
auto j = j_result.value();
|
||||
|
||||
auto chat_response = json_to<ChatResponse>(j);
|
||||
if (!chat_response.has_value())
|
||||
@@ -233,10 +234,11 @@ std::expected<std::vector<std::string>, std::string> OllamaClient::list_models()
|
||||
}
|
||||
std::string response_str = response_result.value();
|
||||
|
||||
auto j = parse_json_safely(response_str);
|
||||
if (j.is_discarded()) {
|
||||
return std::unexpected("JSON parse error: input was discarded");
|
||||
auto j_result = parse_json_safely(response_str);
|
||||
if (!j_result) {
|
||||
return std::unexpected("JSON parse error: " + j_result.error());
|
||||
}
|
||||
auto j = j_result.value();
|
||||
auto result = json_to<ModelListResponse>(j);
|
||||
if (!result) {
|
||||
return std::unexpected(std::string("JSON parse error: ") + result.error());
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
#include "rebase_engine.hpp"
|
||||
#include "json_types.hpp"
|
||||
#include "utility.hpp"
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include <unordered_set>
|
||||
|
||||
static std::vector<std::string> split_lines(const std::string& content) {
|
||||
std::vector<std::string> lines;
|
||||
std::istringstream stream(content);
|
||||
std::string line;
|
||||
while (std::getline(stream, line)) {
|
||||
lines.push_back(line);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
static std::string format_error(size_t line_num, const std::string& message) {
|
||||
return "line " + std::to_string(line_num + 1) + ": " + message;
|
||||
}
|
||||
|
||||
static std::expected<RebaseCommand, std::string> parse_command(const std::string& cmd) {
|
||||
if (cmd == "pick") return RebaseCommand::Pick;
|
||||
if (cmd == "edit") return RebaseCommand::Edit;
|
||||
if (cmd == "drop") return RebaseCommand::Drop;
|
||||
return std::unexpected("invalid command: '" + cmd + "'");
|
||||
}
|
||||
|
||||
std::expected<std::vector<RebaseLine>, std::string> RebaseEngine::parse_rebase_file(
|
||||
const std::string& content, size_t history_size) const {
|
||||
|
||||
std::vector<RebaseLine> lines;
|
||||
std::unordered_set<size_t> seen_indices;
|
||||
|
||||
auto file_lines = split_lines(content);
|
||||
|
||||
for (size_t line_num = 0; line_num < file_lines.size(); ++line_num) {
|
||||
std::string line = trim(file_lines[line_num]);
|
||||
|
||||
// Skip empty lines (treated as drops) and comment lines
|
||||
if (line.empty() || line[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse: <command> <index> [<role>] <content...>
|
||||
std::istringstream iss(line);
|
||||
std::string cmd_str;
|
||||
std::string index_str;
|
||||
std::string role_str;
|
||||
|
||||
if (!(iss >> cmd_str)) {
|
||||
return std::unexpected(format_error(line_num, "missing command"));
|
||||
}
|
||||
|
||||
if (!(iss >> index_str)) {
|
||||
return std::unexpected(format_error(line_num, "missing index"));
|
||||
}
|
||||
|
||||
if (!(iss >> role_str)) {
|
||||
return std::unexpected(format_error(line_num, "missing role"));
|
||||
}
|
||||
|
||||
// Parse command
|
||||
auto cmd_result = parse_command(cmd_str);
|
||||
if (!cmd_result) {
|
||||
return std::unexpected(format_error(line_num, cmd_result.error()));
|
||||
}
|
||||
|
||||
// Parse index
|
||||
size_t index;
|
||||
auto index_result = string_toul(index_str);
|
||||
if (!index_result) {
|
||||
return std::unexpected(format_error(line_num, "invalid index: '" + index_str + "'"));
|
||||
}
|
||||
index = index_result.value();
|
||||
|
||||
// Validate index is in range
|
||||
if (index >= history_size) {
|
||||
return std::unexpected(format_error(line_num, "index " + std::to_string(index) + " out of range (history size: " + std::to_string(history_size) + ")"));
|
||||
}
|
||||
|
||||
// Check for duplicate indices
|
||||
if (seen_indices.count(index)) {
|
||||
return std::unexpected(format_error(line_num, "duplicate index " + std::to_string(index)));
|
||||
}
|
||||
|
||||
// Parse and validate role
|
||||
// Role must be in format [user], [assistant], or [tool]
|
||||
if (role_str.size() < 3 || role_str.front() != '[' || role_str.back() != ']') {
|
||||
return std::unexpected(format_error(line_num, "malformed role: '" + role_str + "'"));
|
||||
}
|
||||
|
||||
std::string role_content = role_str.substr(1, role_str.size() - 2);
|
||||
if (role_content != "user" && role_content != "assistant" && role_content != "tool") {
|
||||
return std::unexpected(format_error(line_num, "invalid role: '" + role_str + "'"));
|
||||
}
|
||||
|
||||
// Valid line - add to results
|
||||
seen_indices.insert(index);
|
||||
lines.push_back({cmd_result.value(), index, role_content, ""});
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
std::string HistoryEditor::get_first_line(const std::string& content) const {
|
||||
auto pos = content.find('\n');
|
||||
if (pos == std::string::npos) {
|
||||
return content;
|
||||
}
|
||||
return content.substr(0, pos);
|
||||
}
|
||||
|
||||
std::string HistoryEditor::get_message_display(const Message& msg) const {
|
||||
// Check for assistant messages with tool_calls only
|
||||
if (msg.role == "assistant" && !msg.tool_calls.empty()) {
|
||||
if (msg.tool_calls.size() == 1) {
|
||||
return "[tool_call: " + msg.tool_calls[0].function.name + "]";
|
||||
} else {
|
||||
// Multiple tool calls - show first one
|
||||
return "[tool_call: " + msg.tool_calls[0].function.name + " ...]";
|
||||
}
|
||||
}
|
||||
|
||||
// Get content from message
|
||||
std::string content = msg.get_content_string();
|
||||
|
||||
// If no content, show (empty)
|
||||
if (content.empty()) {
|
||||
return "(empty)";
|
||||
}
|
||||
|
||||
// Return first line of content
|
||||
return get_first_line(content);
|
||||
}
|
||||
|
||||
std::string HistoryEditor::build_rebase_content(const std::vector<Message>& history) const {
|
||||
std::ostringstream oss;
|
||||
|
||||
// Header comment block
|
||||
oss << "# History Rebase Editor\n";
|
||||
oss << "#\n";
|
||||
oss << "# Commands:\n";
|
||||
oss << "# pick - keep the message unchanged\n";
|
||||
oss << "# edit - modify the message content and/or role\n";
|
||||
oss << "# drop - remove the message from history\n";
|
||||
oss << "#\n";
|
||||
oss << "# Each line format: <command> <index> [<role>] <display_content>\n";
|
||||
oss << "# - index: 0-based message index\n";
|
||||
oss << "# - role: [user], [assistant], or [tool] (editable)\n";
|
||||
oss << "# - display_content: first line of message (display only, not editable here)\n";
|
||||
oss << "#\n";
|
||||
oss << "# Lines starting with # are comments and will be ignored.\n";
|
||||
oss << "# Empty lines are treated as drops.\n";
|
||||
oss << "#\n";
|
||||
oss << "-- >8 --\n";
|
||||
|
||||
// Format each message
|
||||
for (size_t i = 0; i < history.size(); ++i) {
|
||||
const auto& msg = history[i];
|
||||
std::string display = get_message_display(msg);
|
||||
oss << "pick " << i << " [" << msg.role << "] " << display << "\n";
|
||||
}
|
||||
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string HistoryEditor::build_edit_prompt(const std::vector<Message>& history,
|
||||
size_t edit_index,
|
||||
const std::string& role) const {
|
||||
std::ostringstream oss;
|
||||
|
||||
// Show previous message as context
|
||||
if (edit_index == 0) {
|
||||
oss << "# (no previous message)\n";
|
||||
} else {
|
||||
const auto& prev_msg = history[edit_index - 1];
|
||||
oss << "# Previous message:\n\n";
|
||||
oss << "[" << (edit_index - 1) << "] " << prev_msg.role << ": " << prev_msg.content << "\n";
|
||||
}
|
||||
|
||||
// Edit prompt
|
||||
oss << "\nEdit message " << edit_index << " (role: " << role << ") below:\n";
|
||||
|
||||
// Separator - editable content goes after this
|
||||
oss << "\n-- >8 --\n";
|
||||
|
||||
// Current content (editable)
|
||||
if (edit_index < history.size()) {
|
||||
oss << history[edit_index].content;
|
||||
}
|
||||
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::expected<std::vector<Message>, std::string> RebaseEngine::apply_rebase(
|
||||
const std::vector<RebaseLine>& lines,
|
||||
const std::vector<Message>& history) const {
|
||||
|
||||
// Build a map from index to RebaseLine for quick lookup
|
||||
std::unordered_map<size_t, const RebaseLine*> command_map;
|
||||
for (const auto& line : lines) {
|
||||
command_map[line.index] = &line;
|
||||
}
|
||||
|
||||
std::vector<Message> result;
|
||||
|
||||
// Process messages in forward order (lowest index first)
|
||||
for (size_t i = 0; i < history.size(); ++i) {
|
||||
auto it = command_map.find(i);
|
||||
|
||||
if (it == command_map.end()) {
|
||||
// No command for this index - implicit drop (message is removed)
|
||||
// Special case: if no commands at all were parsed, keep all messages
|
||||
if (command_map.empty()) {
|
||||
result.push_back(history[i]);
|
||||
}
|
||||
// Otherwise, the message is dropped (do nothing)
|
||||
} else {
|
||||
const auto& cmd = it->second;
|
||||
|
||||
switch (cmd->command) {
|
||||
case RebaseCommand::Pick: {
|
||||
// Keep message unchanged
|
||||
result.push_back(history[i]);
|
||||
break;
|
||||
}
|
||||
case RebaseCommand::Drop: {
|
||||
// Remove message from result (don't add it)
|
||||
break;
|
||||
}
|
||||
case RebaseCommand::Edit: {
|
||||
// Create a copy and update role and content
|
||||
Message edited = history[i];
|
||||
edited.role = cmd->role;
|
||||
// Update content only if it was explicitly edited
|
||||
if (cmd->content_edited) {
|
||||
edited.content = cmd->content;
|
||||
}
|
||||
result.push_back(std::move(edited));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::expected<std::vector<Message>, std::string> HistoryEditor::edit_history_interactive(
|
||||
const std::vector<Message>& history) const {
|
||||
|
||||
// Step 1: Build rebase content and spawn editor
|
||||
std::string rebase_content = build_rebase_content(history);
|
||||
auto editor_result = spawn_editor(rebase_content);
|
||||
if (!editor_result) {
|
||||
return std::unexpected("Failed to run rebase editor: " + editor_result.error());
|
||||
}
|
||||
std::string rebase_output = editor_result.value();
|
||||
|
||||
// Step 2: Parse and validate rebase commands
|
||||
RebaseEngine engine;
|
||||
auto parse_result = engine.parse_rebase_file(rebase_output, history.size());
|
||||
if (!parse_result) {
|
||||
return std::unexpected("Rebase parse error: " + parse_result.error());
|
||||
}
|
||||
std::vector<RebaseLine> lines = parse_result.value();
|
||||
|
||||
// Step 3: For each edit command (in forward order), build edit prompt and spawn editor
|
||||
// First, sort lines by index to ensure forward order
|
||||
std::sort(lines.begin(), lines.end(), [](const RebaseLine& a, const RebaseLine& b) {
|
||||
return a.index < b.index;
|
||||
});
|
||||
|
||||
// Collect edit indices first (forward order)
|
||||
std::vector<size_t> edit_indices;
|
||||
for (const auto& line : lines) {
|
||||
if (line.command == RebaseCommand::Edit) {
|
||||
edit_indices.push_back(line.index);
|
||||
}
|
||||
}
|
||||
|
||||
// For each edit, spawn editor to get new content
|
||||
for (size_t edit_idx : edit_indices) {
|
||||
// Find the RebaseLine for this index
|
||||
const RebaseLine* edit_line = nullptr;
|
||||
for (const auto& line : lines) {
|
||||
if (line.index == edit_idx && line.command == RebaseCommand::Edit) {
|
||||
edit_line = &line;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!edit_line) continue;
|
||||
|
||||
// Build edit prompt with context
|
||||
std::string prompt = build_edit_prompt(history, edit_idx, edit_line->role);
|
||||
|
||||
// Spawn editor to get new content
|
||||
auto edit_result = spawn_editor(prompt);
|
||||
if (!edit_result) {
|
||||
return std::unexpected("Failed to run edit editor for message " +
|
||||
std::to_string(edit_idx) + ": " + edit_result.error());
|
||||
}
|
||||
|
||||
// Update the RebaseLine with the new content
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Apply all changes
|
||||
auto apply_result = engine.apply_rebase(lines, history);
|
||||
if (!apply_result) {
|
||||
return std::unexpected("Failed to apply rebase: " + apply_result.error());
|
||||
}
|
||||
|
||||
return apply_result.value();
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
#include "utility.hpp"
|
||||
#include <fstream>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <random>
|
||||
#include <cctype>
|
||||
|
||||
std::string trim(const std::string& s) {
|
||||
auto start = s.find_first_not_of(" \t\n\r");
|
||||
if (start == std::string::npos) return "";
|
||||
auto end = s.find_last_not_of(" \t\n\r");
|
||||
return s.substr(start, end - start + 1);
|
||||
}
|
||||
|
||||
std::string random_string(size_t length) {
|
||||
static const char chars[] =
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"0123456789";
|
||||
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
static std::uniform_int_distribution<> dis(0, sizeof(chars) - 2);
|
||||
|
||||
std::string s;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
s += chars[dis(gen)];
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
std::string random_file_name(size_t length) {
|
||||
std::filesystem::path temp_dir = std::filesystem::temp_directory_path();
|
||||
std::filesystem::path temp_file = temp_dir / ("tmp_" + random_string(length));
|
||||
return temp_file.string();
|
||||
}
|
||||
|
||||
std::expected<std::string, std::string> spawn_editor(const std::string& initial_content) {
|
||||
auto file_path = random_file_name();
|
||||
auto cleanup = Defer([&](){
|
||||
std::filesystem::remove(file_path);
|
||||
});
|
||||
|
||||
{
|
||||
std::ofstream file(file_path);
|
||||
if (!file.is_open()) {
|
||||
return std::unexpected("Failed to open temp file for editor.");
|
||||
}
|
||||
file << initial_content;
|
||||
file.close();
|
||||
|
||||
const char* editor_env = getenv("VISUAL");
|
||||
if (!editor_env) editor_env = getenv("EDITOR");
|
||||
if (!editor_env) {
|
||||
return std::unexpected("Neither $VISUAL nor $EDITOR is set.");
|
||||
}
|
||||
|
||||
std::string cmd = std::string(editor_env) + " " + file_path;
|
||||
int res = system(cmd.c_str());
|
||||
if (res != 0) {
|
||||
return std::unexpected("Editor exited with error.");
|
||||
}
|
||||
|
||||
std::ifstream infile(file_path);
|
||||
std::string content((std::istreambuf_iterator<char>(infile)), std::istreambuf_iterator<char>());
|
||||
|
||||
const std::string seperator = "\n-- >8 --\n";
|
||||
size_t pos = content.find(seperator);
|
||||
if (pos == std::string::npos) {
|
||||
return std::unexpected("Scissors not found.");
|
||||
}
|
||||
|
||||
size_t last_pos = std::string::npos;
|
||||
while (pos != std::string::npos) {
|
||||
last_pos = pos;
|
||||
pos = content.find(seperator, pos + 1);
|
||||
}
|
||||
|
||||
std::string extracted = content.substr(last_pos + seperator.size());
|
||||
extracted = trim(extracted);
|
||||
|
||||
return extracted;
|
||||
}
|
||||
}
|
||||
|
||||
std::expected<unsigned long, std::string> string_toul(std::string const& str) {
|
||||
size_t pos = 0;
|
||||
try {
|
||||
unsigned long result = std::stoul(str, &pos);
|
||||
|
||||
// Check if the entire string was consumed
|
||||
while (pos < str.size() && std::isspace(static_cast<unsigned char>(str[pos]))) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (pos != str.size()) {
|
||||
return std::unexpected("trailing characters after number");
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (const std::exception& e) {
|
||||
return std::unexpected(e.what());
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,90 @@ add_dr_test(test_json_variant)
|
||||
# Add expected error handling tests
|
||||
add_dr_test(test_expected_errors)
|
||||
|
||||
# Add rebase parser tests (split into individual tests)
|
||||
add_dr_test(test_rebase_parser_all_commands)
|
||||
add_dr_test(test_rebase_parser_with_comments)
|
||||
add_dr_test(test_rebase_parser_with_empty_lines)
|
||||
add_dr_test(test_rebase_parser_invalid_command)
|
||||
add_dr_test(test_rebase_parser_index_out_of_range)
|
||||
add_dr_test(test_rebase_parser_duplicate_indices)
|
||||
add_dr_test(test_rebase_parser_malformed_role)
|
||||
add_dr_test(test_rebase_parser_invalid_role_value)
|
||||
add_dr_test(test_rebase_parser_first_error_returned)
|
||||
add_dr_test(test_rebase_parser_empty_rebase)
|
||||
add_dr_test(test_rebase_parser_only_comments)
|
||||
add_dr_test(test_rebase_parser_all_valid_roles)
|
||||
add_dr_test(test_rebase_parser_invalid_command_digit)
|
||||
add_dr_test(test_rebase_parser_invalid_index)
|
||||
add_dr_test(test_rebase_parser_missing_role)
|
||||
add_dr_test(test_rebase_parser_whitespace_variations)
|
||||
add_dr_test(test_rebase_parser_comments_and_empty_lines_mixed)
|
||||
|
||||
# Add rebase content formatting tests (split into individual tests)
|
||||
add_dr_test(test_rebase_content_basic_messages)
|
||||
add_dr_test(test_rebase_content_multiline_content)
|
||||
add_dr_test(test_rebase_content_tool_call_display)
|
||||
add_dr_test(test_rebase_content_tool_message)
|
||||
add_dr_test(test_rebase_content_empty_message)
|
||||
add_dr_test(test_rebase_content_header_comments)
|
||||
add_dr_test(test_rebase_content_multiple_tool_calls)
|
||||
add_dr_test(test_rebase_content_message_indices)
|
||||
add_dr_test(test_rebase_content_scissors_line)
|
||||
add_dr_test(test_rebase_content_special_characters)
|
||||
add_dr_test(test_rebase_content_unicode_characters)
|
||||
add_dr_test(test_rebase_content_very_long_content)
|
||||
|
||||
# Add rebase apply tests (split into individual tests)
|
||||
add_dr_test(test_rebase_apply_pick_preserves)
|
||||
add_dr_test(test_rebase_apply_drop_removes)
|
||||
add_dr_test(test_rebase_apply_edit_updates_role)
|
||||
add_dr_test(test_rebase_apply_edit_preserves_fields)
|
||||
add_dr_test(test_rebase_apply_implicit_pick)
|
||||
add_dr_test(test_rebase_apply_mixed_commands)
|
||||
add_dr_test(test_rebase_apply_empty_lines_preserves)
|
||||
add_dr_test(test_rebase_apply_lenient_orphaned_results)
|
||||
add_dr_test(test_rebase_apply_edit_preserves_tool_calls)
|
||||
add_dr_test(test_rebase_apply_deleting_lines_drops)
|
||||
add_dr_test(test_rebase_apply_all_lines_deleted_keeps)
|
||||
|
||||
# Add edit prompt tests (split from test_edit_prompt.sh)
|
||||
add_dr_test(test_edit_prompt_with_context)
|
||||
add_dr_test(test_edit_prompt_first_message)
|
||||
add_dr_test(test_edit_prompt_separator_placement)
|
||||
add_dr_test(test_edit_prompt_multiline_context)
|
||||
add_dr_test(test_edit_prompt_tool_role)
|
||||
add_dr_test(test_edit_prompt_middle_message)
|
||||
|
||||
# Add edit history flag tests (split from test_edit_history_flag.sh)
|
||||
add_dr_test(test_edit_history_flag_requires_F_short)
|
||||
add_dr_test(test_edit_history_flag_requires_F_long)
|
||||
add_dr_test(test_edit_history_flag_empty_file_short)
|
||||
add_dr_test(test_edit_history_flag_no_history)
|
||||
add_dr_test(test_edit_history_flag_empty_file_long)
|
||||
|
||||
# Add edit history integration tests (split into individual tests)
|
||||
add_dr_test(test_edit_history_pick_all_messages)
|
||||
add_dr_test(test_edit_history_no_edits_unchanged)
|
||||
add_dr_test(test_edit_history_drop_single_message)
|
||||
add_dr_test(test_edit_history_edit_message_content)
|
||||
add_dr_test(test_edit_history_change_message_role)
|
||||
add_dr_test(test_edit_history_delete_line_equals_drop)
|
||||
add_dr_test(test_edit_history_edit_first_message)
|
||||
add_dr_test(test_edit_history_prompt_noop)
|
||||
add_dr_test(test_edit_history_empty_history_rejected)
|
||||
add_dr_test(test_edit_history_invalid_command_error)
|
||||
add_dr_test(test_edit_history_invalid_index_error)
|
||||
add_dr_test(test_edit_history_invalid_role_error)
|
||||
add_dr_test(test_edit_history_multiple_edits)
|
||||
add_dr_test(test_edit_history_then_continue)
|
||||
add_dr_test(test_edit_history_remove_all_content)
|
||||
add_dr_test(test_edit_history_multiple_consecutive_edits)
|
||||
add_dr_test(test_edit_history_special_characters)
|
||||
add_dr_test(test_edit_history_very_long_content)
|
||||
add_dr_test(test_edit_history_assistant_with_tool_calls)
|
||||
add_dr_test(test_edit_history_drop_all_messages)
|
||||
add_dr_test(test_edit_history_unicode_characters)
|
||||
|
||||
# Add base64 encoding tests
|
||||
add_dr_test(test_base64_encode)
|
||||
|
||||
|
||||
@@ -2,6 +2,33 @@
|
||||
# $1 is the file path passed to the editor
|
||||
FILE=$1
|
||||
|
||||
# Support sequential invocations using a counter file
|
||||
# Track invocations in $TEST_TEMP_DIR/.editor_invocation_count
|
||||
# Copy from $TEST_TEMP_DIR/.editor_input_$INVOCATION to the editor file
|
||||
if [ -n "$TEST_TEMP_DIR" ] && [ -f "$TEST_TEMP_DIR/.editor_input_0" ]; then
|
||||
# Initialize counter file if it doesn't exist
|
||||
if [ ! -f "$TEST_TEMP_DIR/.editor_invocation_count" ]; then
|
||||
echo "0" > "$TEST_TEMP_DIR/.editor_invocation_count"
|
||||
fi
|
||||
|
||||
# Read current invocation count
|
||||
INVOCATION=$(cat "$TEST_TEMP_DIR/.editor_invocation_count")
|
||||
|
||||
# Check if there's a staged input file for this invocation
|
||||
STAGED_INPUT="$TEST_TEMP_DIR/.editor_input_$INVOCATION"
|
||||
if [ -f "$STAGED_INPUT" ]; then
|
||||
# Copy staged content to the editor file
|
||||
cp "$STAGED_INPUT" "$FILE"
|
||||
fi
|
||||
|
||||
# Increment the counter for next invocation
|
||||
echo $((INVOCATION + 1)) > "$TEST_TEMP_DIR/.editor_invocation_count"
|
||||
|
||||
# For inspecting by tests
|
||||
cat $FILE > hist.md
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# For inspecting by tests
|
||||
cat $FILE > hist.md
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ $DRHARNESS -n -F reasoning_content.json -m "Test" -d < reasoning_content
|
||||
|
||||
|
||||
|
||||
if [[ "$(cat reasoning.json | jq .history[1].reasoning -r)" != "I should do X because Y" ]]; then
|
||||
cat reasoning.json | jq
|
||||
if [[ "$(dr_reasoning reasoning.json 1)" != "I should do X because Y" ]]; then
|
||||
echo "reasoning mismatch"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$(cat reasoning_content.json | jq .history[1].reasoning_content -r)" != "I should do X because Y" ]]; then
|
||||
cat reasoning_content.json | jq
|
||||
if [[ "$(dr_reasoning_content reasoning_content.json 1)" != "I should do X because Y" ]]; then
|
||||
echo "reasoning_content mismatch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -144,6 +144,223 @@ function dr_result() {
|
||||
}
|
||||
export -f dr_result
|
||||
|
||||
function dr_role() {
|
||||
local chat_file="$1"
|
||||
local entry="$2"
|
||||
jq -r --arg e "$entry" '.history[$e | tonumber].role' "$chat_file" 2>/dev/null || echo ""
|
||||
}
|
||||
export -f dr_role
|
||||
|
||||
function dr_reasoning() {
|
||||
local chat_file="$1"
|
||||
local entry="$2"
|
||||
jq -r --arg e "$entry" '.history[$e | tonumber].reasoning // ""' "$chat_file" 2>/dev/null || echo ""
|
||||
}
|
||||
export -f dr_reasoning
|
||||
|
||||
function dr_reasoning_content() {
|
||||
local chat_file="$1"
|
||||
local entry="$2"
|
||||
jq -r --arg e "$entry" '.history[$e | tonumber].reasoning_content // ""' "$chat_file" 2>/dev/null || echo ""
|
||||
}
|
||||
export -f dr_reasoning_content
|
||||
|
||||
# Create a chat file with history messages
|
||||
# Usage: create_chat_file <filename> <role> <content> [role content ...]
|
||||
# Example: create_chat_file chat.json user "hello" assistant "hi there"
|
||||
function create_chat_file() {
|
||||
local file="$1"
|
||||
shift
|
||||
create_history_json "$@" | jq '{history: ., tools: []}' > "$file"
|
||||
}
|
||||
export -f create_chat_file
|
||||
|
||||
# Create a JSON array of history messages (for -T flag testing)
|
||||
# Usage: create_history_json role content [role content ...]
|
||||
# Example: create_history_json user "hello" assistant "hi there"
|
||||
function create_history_json() {
|
||||
local history="[]"
|
||||
while [ $# -gt 0 ]; do
|
||||
local role="$1"
|
||||
local content="$2"
|
||||
shift 2
|
||||
history=$(echo "$history" | jq --arg r "$role" --arg c "$content" '. + [{"role": $r, "content": $c}]')
|
||||
done
|
||||
echo "$history"
|
||||
}
|
||||
export -f create_history_json
|
||||
|
||||
# Helper functions for rebase parser testing
|
||||
|
||||
# Get number of lines in rebase JSON output
|
||||
function rebase_line_count() {
|
||||
local output="$1"
|
||||
echo "$output" | jq 'length'
|
||||
}
|
||||
export -f rebase_line_count
|
||||
|
||||
# Get command at position in rebase JSON output
|
||||
function rebase_command_at() {
|
||||
local output="$1"
|
||||
local pos="$2"
|
||||
echo "$output" | jq -r ".[$pos | tonumber].command"
|
||||
}
|
||||
export -f rebase_command_at
|
||||
|
||||
# Get index at position in rebase JSON output
|
||||
function rebase_index_at() {
|
||||
local output="$1"
|
||||
local pos="$2"
|
||||
echo "$output" | jq -r ".[$pos | tonumber].index"
|
||||
}
|
||||
export -f rebase_index_at
|
||||
|
||||
# Get role at position in rebase JSON output
|
||||
function rebase_role_at() {
|
||||
local output="$1"
|
||||
local pos="$2"
|
||||
echo "$output" | jq -r ".[$pos | tonumber].role"
|
||||
}
|
||||
export -f rebase_role_at
|
||||
|
||||
# Helper functions for rebase apply testing
|
||||
|
||||
# Create a rebase line JSON object
|
||||
# Usage: create_rebase_line <command> <index> <role>
|
||||
# Example: create_rebase_line pick 0 user
|
||||
function create_rebase_line() {
|
||||
local cmd="$1"
|
||||
local idx="$2"
|
||||
local role="$3"
|
||||
jq -n --arg c "$cmd" --argjson i "$idx" --arg r "$role" '{"command": $c, "index": $i, "role": $r}'
|
||||
}
|
||||
export -f create_rebase_line
|
||||
|
||||
# Create a JSON array of rebase lines
|
||||
# Usage: create_rebase_lines [command index role ...]
|
||||
# Example: create_rebase_lines pick 0 user drop 1 assistant edit 2 tool
|
||||
function create_rebase_lines() {
|
||||
local lines="[]"
|
||||
while [ $# -gt 0 ]; do
|
||||
local cmd="$1"
|
||||
local idx="$2"
|
||||
local role="$3"
|
||||
shift 3
|
||||
local line
|
||||
line=$(create_rebase_line "$cmd" "$idx" "$role")
|
||||
lines=$(echo "$lines" | jq --argjson l "$line" '. + [$l]')
|
||||
done
|
||||
echo "$lines"
|
||||
}
|
||||
export -f create_rebase_lines
|
||||
|
||||
# Add a field to a message JSON object
|
||||
function add_message_field() {
|
||||
local msg="$1"
|
||||
local field="$2"
|
||||
local value="$3"
|
||||
case "$field" in
|
||||
reasoning)
|
||||
echo "$msg" | jq --arg v "$value" '. + {reasoning: $v}'
|
||||
;;
|
||||
reasoning_content)
|
||||
echo "$msg" | jq --arg v "$value" '. + {reasoning_content: $v}'
|
||||
;;
|
||||
tool_call_id)
|
||||
echo "$msg" | jq --arg v "$value" '. + {tool_call_id: $v}'
|
||||
;;
|
||||
tool_call)
|
||||
local call_id="$3"
|
||||
local call_name="$4"
|
||||
local call_args="$5"
|
||||
echo "$msg" | jq --arg id "$call_id" --arg name "$call_name" --arg args "$call_args" \
|
||||
'.tool_calls = ((.tool_calls // []) + [{"index": 0, "id": $id, "type": "function", "function": {"name": $name, "arguments": $args}}])'
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unknown field '$field'" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
export -f add_message_field
|
||||
|
||||
# Create a message JSON object with optional fields
|
||||
# Usage: create_message_json <role> <content> [field value ...]
|
||||
# Supported fields: reasoning, reasoning_content, tool_call_id, tool_call
|
||||
# tool_call takes 3 args: tool_call <id> <name> <arguments>
|
||||
# Example: create_message_json user "hello"
|
||||
# Example: create_message_json user "hello" reasoning "thought" tool_call_id "call_123"
|
||||
# Example: create_message_json assistant "" tool_call "call_1" "test" "{}"
|
||||
function create_message_json() {
|
||||
local role="$1"
|
||||
local content="$2"
|
||||
shift 2
|
||||
|
||||
local msg
|
||||
msg=$(jq -n --arg r "$role" --arg c "$content" '{"role": $r, "content": $c}')
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
local field="$1"
|
||||
if [ "$field" = "tool_call" ]; then
|
||||
local call_id="$2"
|
||||
local call_name="$3"
|
||||
local call_args="$4"
|
||||
shift 4
|
||||
msg=$(add_message_field "$msg" "$field" "$call_id" "$call_name" "$call_args")
|
||||
else
|
||||
local value="$2"
|
||||
shift 2
|
||||
msg=$(add_message_field "$msg" "$field" "$value")
|
||||
fi
|
||||
done
|
||||
echo "$msg"
|
||||
}
|
||||
export -f create_message_json
|
||||
|
||||
# Check if a string is a known message field name
|
||||
function is_message_field() {
|
||||
case "$1" in
|
||||
reasoning|reasoning_content|tool_call_id|tool_call) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
export -f is_message_field
|
||||
|
||||
# Create a JSON array of messages
|
||||
# Usage: create_messages_json <role> <content> [field value ...] [role content ...]
|
||||
# Each message: role content [field value ...]
|
||||
# Supported fields: reasoning, reasoning_content, tool_call_id, tool_call
|
||||
# tool_call takes 3 args: tool_call <id> <name> <arguments>
|
||||
# Example: create_messages_json user "hello" assistant "hi there"
|
||||
# Example: create_messages_json user "hello" reasoning "thought" assistant "hi"
|
||||
function create_messages_json() {
|
||||
local messages="[]"
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
local role="$1"
|
||||
local content="$2"
|
||||
shift 2
|
||||
|
||||
# Collect field-value pairs for this message into an array
|
||||
local -a field_args=()
|
||||
while [ $# -gt 0 ] && is_message_field "$1"; do
|
||||
if [ "$1" = "tool_call" ]; then
|
||||
field_args+=("$1" "$2" "$3" "$4")
|
||||
shift 4
|
||||
else
|
||||
field_args+=("$1" "$2")
|
||||
shift 2
|
||||
fi
|
||||
done
|
||||
|
||||
local msg
|
||||
msg=$(create_message_json "$role" "$content" "${field_args[@]}")
|
||||
messages=$(echo "$messages" | jq --argjson m "$msg" '. + [$m]')
|
||||
done
|
||||
echo "$messages"
|
||||
}
|
||||
export -f create_messages_json
|
||||
|
||||
function create_state() {
|
||||
local file="$1"
|
||||
shift
|
||||
|
||||
@@ -16,7 +16,7 @@ if [ "${OUTPUT}" != "" ]; then
|
||||
fi
|
||||
|
||||
# Test 3: Base64 decoding
|
||||
OUTPUT=$("${DRHARNESS}" -b "SGVsbG8sIFdvcmxkIQ==")
|
||||
OUTPUT=$("${DRHARNESS}" -K "SGVsbG8sIFdvcmxkIQ==")
|
||||
if [ "${OUTPUT}" != "Hello, World!" ]; then
|
||||
echo "FAIL: base64_decode - expected 'Hello, World!', got '${OUTPUT}'"
|
||||
exit 1
|
||||
@@ -25,7 +25,7 @@ fi
|
||||
# Test 4: Round-trip encoding/decoding
|
||||
ORIGINAL="Test string with special chars: !@#$%^&*()"
|
||||
ENCODED=$("${DRHARNESS}" -B "${ORIGINAL}")
|
||||
DECODED=$("${DRHARNESS}" -b "${ENCODED}")
|
||||
DECODED=$("${DRHARNESS}" -K "${ENCODED}")
|
||||
if [ "${DECODED}" != "${ORIGINAL}" ]; then
|
||||
echo "FAIL: round-trip - expected '${ORIGINAL}', got '${DECODED}'"
|
||||
exit 1
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_assistant_with_tool_calls.sh - Editing assistant messages that contain tool_calls
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# 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":[{"index":0,"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] calling tool [tool_call: test]
|
||||
pick 2 [tool] result
|
||||
EDITEOF
|
||||
|
||||
# Stage the edit prompt response
|
||||
cat > .editor_input_1 << 'EDITEOF'
|
||||
# Previous message:
|
||||
|
||||
[0] user: hello
|
||||
|
||||
Edit message 1 (role: assistant) below:
|
||||
|
||||
-- >8 --
|
||||
edited assistant 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: edit_assistant_with_tool_calls - expected exit code 0, got $EXIT_CODE"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify history still has 3 messages
|
||||
len=$(history_length chat.json)
|
||||
if [ "$len" != "3" ]; then
|
||||
echo "FAIL: edit_assistant_with_tool_calls - expected 3 messages, got $len"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify message 1 content was edited
|
||||
msg1_content=$(dr_result chat.json 1)
|
||||
if [ "$msg1_content" != "edited assistant message" ]; then
|
||||
echo "FAIL: edit_assistant_with_tool_calls - message 1 content not updated correctly"
|
||||
echo "Expected: 'edited assistant message', got: '$msg1_content'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify tool_calls are preserved
|
||||
if ! cat chat.json | jq -e '.history[1].tool_calls' > /dev/null 2>&1; then
|
||||
echo "FAIL: edit_assistant_with_tool_calls - tool_calls not preserved"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: edit_assistant_with_tool_calls"
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_change_message_role.sh - Change message role
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# 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 role to user)
|
||||
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 [user] hi there
|
||||
EDITEOF
|
||||
|
||||
# Stage the edit prompt response (keep same content)
|
||||
cat > .editor_input_1 << 'EDITEOF'
|
||||
# Previous message:
|
||||
|
||||
[0] user: hello
|
||||
|
||||
Edit message 1 (role: user) below:
|
||||
|
||||
-- >8 --
|
||||
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: change_message_role - expected exit code 0, got $EXIT_CODE"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify message 1 role was changed
|
||||
msg1_role=$(dr_role chat.json 1)
|
||||
if [ "$msg1_role" != "user" ]; then
|
||||
echo "FAIL: change_message_role - message 1 role should be 'user', got '$msg1_role'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: change_message_role"
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_delete_line_equals_drop.sh - Delete line equals drop (implicit drop for messages not mentioned)
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# Create a chat file with two messages
|
||||
create_chat_file chat.json user "hello" assistant "hi there"
|
||||
|
||||
# Stage rebase editor output with only message 0 mentioned
|
||||
# Messages not mentioned are implicitly dropped
|
||||
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
|
||||
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: delete_line_equals_drop - expected exit code 0, got $EXIT_CODE"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Messages not mentioned are implicitly dropped, so we should have 1 message
|
||||
len=$(history_length chat.json)
|
||||
if [ "$len" != "1" ]; then
|
||||
echo "FAIL: delete_line_equals_drop - expected 1 message, got $len"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: delete_line_equals_drop"
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_drop_all_messages.sh - Dropping all messages results in empty history
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# 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: drop_all_messages - expected exit code 0, got $EXIT_CODE"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify history is empty
|
||||
len=$(history_length chat.json)
|
||||
if [ "$len" != "0" ]; then
|
||||
echo "FAIL: drop_all_messages - expected 0 messages, got $len"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: drop_all_messages"
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_drop_single_message.sh - Drop a single message
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# Create a chat file with two messages
|
||||
create_chat_file chat.json user "hello" assistant "hi there"
|
||||
|
||||
# Stage the rebase editor output (drop 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
|
||||
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: drop_single_message - expected exit code 0, got $EXIT_CODE"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify message 1 was dropped
|
||||
len=$(history_length chat.json)
|
||||
if [ "$len" != "1" ]; then
|
||||
echo "FAIL: drop_single_message - expected 1 message, got $len"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg0_content=$(dr_result chat.json 0)
|
||||
if [ "$msg0_content" != "hello" ]; then
|
||||
echo "FAIL: drop_single_message - message 0 content should be unchanged"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: drop_single_message"
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_edit_first_message.sh - Edit first message (no context)
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# Create a chat file with two messages
|
||||
create_chat_file chat.json user "hello" assistant "hi there"
|
||||
|
||||
# Stage the rebase editor output (edit message 0)
|
||||
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 --
|
||||
edit 0 [user] hello
|
||||
pick 1 [assistant] hi there
|
||||
EDITEOF
|
||||
|
||||
# Stage the edit prompt response (message 0 has no previous message)
|
||||
cat > .editor_input_1 << 'EDITEOF'
|
||||
# (no previous message)
|
||||
|
||||
Edit message 0 (role: user) below:
|
||||
|
||||
-- >8 --
|
||||
goodbye
|
||||
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: edit_first_message - expected exit code 0, got $EXIT_CODE"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify message 0 was edited
|
||||
msg0_content=$(dr_result chat.json 0)
|
||||
if [ "$msg0_content" != "goodbye" ]; then
|
||||
echo "FAIL: edit_first_message - message 0 content not updated correctly"
|
||||
echo "Expected: 'goodbye', got: '$msg0_content'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: edit_first_message"
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_edit_message_content.sh - Edit message content
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# 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 (new content for message 1)
|
||||
cat > .editor_input_1 << 'EDITEOF'
|
||||
# Previous message:
|
||||
|
||||
[0] user: hello
|
||||
|
||||
Edit message 1 (role: assistant) below:
|
||||
|
||||
-- >8 --
|
||||
edited content here
|
||||
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: edit_message_content - expected exit code 0, got $EXIT_CODE"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify message 1 was edited
|
||||
len=$(history_length chat.json)
|
||||
if [ "$len" != "2" ]; then
|
||||
echo "FAIL: edit_message_content - expected 2 messages, got $len"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg1_content=$(dr_result chat.json 1)
|
||||
if [ "$msg1_content" != "edited content here" ]; then
|
||||
echo "FAIL: edit_message_content - message 1 content not updated correctly"
|
||||
echo "Expected: 'edited content here', got: '$msg1_content'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: edit_message_content"
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_empty_history_rejected.sh - Empty history rejected
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# Create a chat file with no history
|
||||
create_chat_file chat.json
|
||||
|
||||
# Run drharness with -b flag
|
||||
set +e
|
||||
run_drharness -b -F chat.json
|
||||
EXIT_CODE=$?
|
||||
OUTPUT=$(get_output)
|
||||
set -e
|
||||
|
||||
if [ ${EXIT_CODE} -eq 0 ]; then
|
||||
echo "FAIL: empty_history_rejected - expected non-zero exit code"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "no history"; then
|
||||
echo "FAIL: empty_history_rejected - expected error about no history"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: empty_history_rejected"
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_flag_empty_file_long.sh - Test that --edit-history with empty file fails
|
||||
|
||||
# Create empty file
|
||||
> empty_chat.json
|
||||
|
||||
set +e
|
||||
OUTPUT=$("${DRHARNESS}" --edit-history -F empty_chat.json 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
if [ ${EXIT_CODE} -eq 0 ]; then
|
||||
echo "FAIL: expected non-zero exit code, got 0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: edit_history_flag_empty_file_long"
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_flag_empty_file_short.sh - Test that -b with empty file fails
|
||||
|
||||
# Create empty file
|
||||
> empty_chat.json
|
||||
|
||||
set +e
|
||||
OUTPUT=$("${DRHARNESS}" -b -F empty_chat.json 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
if [ ${EXIT_CODE} -eq 0 ]; then
|
||||
echo "FAIL: expected non-zero exit code, got 0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: edit_history_flag_empty_file_short"
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_flag_no_history.sh - Test that -b with no history fails
|
||||
|
||||
# Create file with valid JSON but empty history
|
||||
create_chat_file no_history.json
|
||||
|
||||
set +e
|
||||
OUTPUT=$("${DRHARNESS}" -b -F no_history.json 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
if [ ${EXIT_CODE} -eq 0 ]; then
|
||||
echo "FAIL: expected non-zero exit code, got 0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "no history"; then
|
||||
echo "FAIL: expected error message about no history"
|
||||
echo "Output was: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: edit_history_flag_no_history"
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_flag_requires_F_long.sh - Test that --edit-history without -F fails
|
||||
|
||||
set +e
|
||||
OUTPUT=$("${DRHARNESS}" --edit-history 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
if [ ${EXIT_CODE} -eq 0 ]; then
|
||||
echo "FAIL: expected non-zero exit code, got 0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "requires -F"; then
|
||||
echo "FAIL: expected error message about -F requirement"
|
||||
echo "Output was: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: edit_history_flag_requires_F_long"
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_flag_requires_F_short.sh - Test that -b without -F fails
|
||||
|
||||
set +e
|
||||
OUTPUT=$("${DRHARNESS}" -b 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
if [ ${EXIT_CODE} -eq 0 ]; then
|
||||
echo "FAIL: expected non-zero exit code, got 0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "requires -F"; then
|
||||
echo "FAIL: expected error message about -F requirement"
|
||||
echo "Output was: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: edit_history_flag_requires_F_short"
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_invalid_command_error.sh - Invalid command error
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# Create a chat file with a message
|
||||
create_chat_file chat.json user "hello"
|
||||
|
||||
# Stage the rebase editor output with invalid command
|
||||
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 --
|
||||
foo 0 [user] 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} -eq 0 ]; then
|
||||
echo "FAIL: invalid_command_error - expected non-zero exit code"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$OUTPUT" | grep -qi "invalid\|error"; then
|
||||
echo "FAIL: invalid_command_error - expected error message"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: invalid_command_error"
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_invalid_index_error.sh - Invalid index error
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# Create a chat file with one message
|
||||
create_chat_file chat.json user "hello"
|
||||
|
||||
# Stage the rebase editor output with out-of-range index
|
||||
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 5 [user] 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} -eq 0 ]; then
|
||||
echo "FAIL: invalid_index_error - expected non-zero exit code"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$OUTPUT" | grep -qi "out of range\|error"; then
|
||||
echo "FAIL: invalid_index_error - expected error about out of range"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: invalid_index_error"
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_invalid_role_error.sh - Invalid role error
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# Create a chat file with a message
|
||||
create_chat_file chat.json user "hello"
|
||||
|
||||
# Stage the rebase editor output with invalid role
|
||||
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 [invalid] 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} -eq 0 ]; then
|
||||
echo "FAIL: invalid_role_error - expected non-zero exit code"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$OUTPUT" | grep -qi "invalid\|error"; then
|
||||
echo "FAIL: invalid_role_error - expected error about invalid role"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: invalid_role_error"
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_multiple_consecutive_edits.sh - Multiple consecutive edits to the same message (changes stack correctly)
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# 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: multiple_consecutive_edits_same_message - first edit expected exit code 0, got $EXIT_CODE"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify first edit was applied
|
||||
msg1_content=$(dr_result chat.json 1)
|
||||
if [ "$msg1_content" != "first edit" ]; then
|
||||
echo "FAIL: multiple_consecutive_edits_same_message - after first edit, expected 'first edit', got: $msg1_content"
|
||||
exit 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: multiple_consecutive_edits_same_message - second edit expected exit code 0, got $EXIT_CODE"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 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: multiple_consecutive_edits_same_message - after second edit, expected 'second edit', got: $msg1_content"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: multiple_consecutive_edits_same_message"
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_multiple_edits.sh - Multiple edits in one session
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# Create a chat file with three messages
|
||||
create_chat_file chat.json user "msg0" assistant "msg1" user "msg2"
|
||||
|
||||
# Stage the rebase editor output (edit messages 0 and 2)
|
||||
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 --
|
||||
edit 0 [user] msg0
|
||||
pick 1 [assistant] msg1
|
||||
edit 2 [user] msg2
|
||||
EDITEOF
|
||||
|
||||
# Stage the edit prompt response for message 0
|
||||
cat > .editor_input_1 << 'EDITEOF'
|
||||
# (no previous message)
|
||||
|
||||
Edit message 0 (role: user) below:
|
||||
|
||||
-- >8 --
|
||||
edited msg0
|
||||
EDITEOF
|
||||
|
||||
# Stage the edit prompt response for message 2
|
||||
cat > .editor_input_2 << 'EDITEOF'
|
||||
# Previous message:
|
||||
|
||||
[1] assistant: msg1
|
||||
|
||||
Edit message 2 (role: user) below:
|
||||
|
||||
-- >8 --
|
||||
edited msg2
|
||||
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: multiple_edits - expected exit code 0, got $EXIT_CODE"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify both messages were edited
|
||||
msg0_content=$(dr_result chat.json 0)
|
||||
if [ "$msg0_content" != "edited msg0" ]; then
|
||||
echo "FAIL: multiple_edits - message 0 not edited correctly"
|
||||
echo "Expected: 'edited msg0', got: '$msg0_content'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg2_content=$(dr_result chat.json 2)
|
||||
if [ "$msg2_content" != "edited msg2" ]; then
|
||||
echo "FAIL: multiple_edits - message 2 not edited correctly"
|
||||
echo "Expected: 'edited msg2', got: '$msg2_content'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: multiple_edits"
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_no_edits_unchanged.sh - No edits, history remains the same
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# Create a chat file with three messages
|
||||
create_chat_file chat.json user "first message" assistant "second message" user "third message"
|
||||
|
||||
# Stage the rebase editor output exactly as generated by build_rebase_content
|
||||
# This simulates the user doing nothing and just saving the editor
|
||||
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] first message
|
||||
pick 1 [assistant] second message
|
||||
pick 2 [user] third 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: no_edits_history_unchanged - expected exit code 0, got $EXIT_CODE"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify history is unchanged - same number of messages
|
||||
len=$(history_length chat.json)
|
||||
if [ "$len" != "3" ]; then
|
||||
echo "FAIL: no_edits_history_unchanged - expected 3 messages, got $len"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify all messages have their original content
|
||||
msg0_content=$(dr_result chat.json 0)
|
||||
if [ "$msg0_content" != "first message" ]; then
|
||||
echo "FAIL: no_edits_history_unchanged - message 0 content changed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg1_content=$(dr_result chat.json 1)
|
||||
if [ "$msg1_content" != "second message" ]; then
|
||||
echo "FAIL: no_edits_history_unchanged - message 1 content changed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg2_content=$(dr_result chat.json 2)
|
||||
if [ "$msg2_content" != "third message" ]; then
|
||||
echo "FAIL: no_edits_history_unchanged - message 2 content changed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify all roles are unchanged
|
||||
msg0_role=$(dr_role chat.json 0)
|
||||
if [ "$msg0_role" != "user" ]; then
|
||||
echo "FAIL: no_edits_history_unchanged - message 0 role changed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg1_role=$(dr_role chat.json 1)
|
||||
if [ "$msg1_role" != "assistant" ]; then
|
||||
echo "FAIL: no_edits_history_unchanged - message 1 role changed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg2_role=$(dr_role chat.json 2)
|
||||
if [ "$msg2_role" != "user" ]; then
|
||||
echo "FAIL: no_edits_history_unchanged - message 2 role changed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: no_edits_history_unchanged"
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_pick_all_messages.sh - Test picking all messages (history unchanged)
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# Create a chat file with two messages
|
||||
create_chat_file chat.json user "hello" assistant "hi there"
|
||||
|
||||
# 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] hello
|
||||
pick 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: pick_all_messages - expected exit code 0, got $EXIT_CODE"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify history is unchanged
|
||||
len=$(history_length chat.json)
|
||||
if [ "$len" != "2" ]; then
|
||||
echo "FAIL: pick_all_messages - expected 2 messages, got $len"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg0_content=$(dr_result chat.json 0)
|
||||
if [ "$msg0_content" != "hello" ]; then
|
||||
echo "FAIL: pick_all_messages - message 0 content changed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg1_content=$(dr_result chat.json 1)
|
||||
if [ "$msg1_content" != "hi there" ]; then
|
||||
echo "FAIL: pick_all_messages - message 1 content changed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: pick_all_messages"
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_prompt_noop.sh - No edits at edit prompt keeps message unchanged
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# 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 (keep same content - no edits)
|
||||
cat > .editor_input_1 << 'EDITEOF'
|
||||
# Previous message:
|
||||
|
||||
[0] user: hello
|
||||
|
||||
Edit message 1 (role: assistant) below:
|
||||
|
||||
-- >8 --
|
||||
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: edit_prompt_noop - expected exit code 0, got $EXIT_CODE"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify message 1 content is unchanged
|
||||
len=$(history_length chat.json)
|
||||
if [ "$len" != "2" ]; then
|
||||
echo "FAIL: edit_prompt_noop - expected 2 messages, got $len"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg1_content=$(dr_result chat.json 1)
|
||||
if [ "$msg1_content" != "hi there" ]; then
|
||||
echo "FAIL: edit_prompt_noop - message 1 content should be unchanged"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: edit_prompt_noop"
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_remove_all_content.sh - Editing a message and removing all content (empty content after edit)
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# 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: edit_remove_all_content - expected exit code 0, got $EXIT_CODE"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify history still has 2 messages
|
||||
len=$(history_length chat.json)
|
||||
if [ "$len" != "2" ]; then
|
||||
echo "FAIL: edit_remove_all_content - expected 2 messages, got $len"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify message 1 content is now empty
|
||||
msg1_content=$(dr_result chat.json 1)
|
||||
if [ "$msg1_content" != "" ]; then
|
||||
echo "FAIL: edit_remove_all_content - message 1 content should be empty, got: $msg1_content"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: edit_remove_all_content"
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_special_characters.sh - Editing messages with special characters in content
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# 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: edit_special_characters - expected exit code 0, got $EXIT_CODE"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify history still has 2 messages
|
||||
len=$(history_length chat.json)
|
||||
if [ "$len" != "2" ]; then
|
||||
echo "FAIL: edit_special_characters - expected 2 messages, got $len"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify message 1 content has the special characters
|
||||
msg1_content=$(dr_result chat.json 1)
|
||||
if [ "$msg1_content" != 'edited with "quotes" and $dollar signs' ]; then
|
||||
echo "FAIL: edit_special_characters - message 1 content mismatch, got: $msg1_content"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: edit_special_characters"
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_then_continue.sh - Edit then continue conversation normally
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# Create a chat file with two messages
|
||||
create_chat_file chat.json user "hello" assistant "hi there"
|
||||
|
||||
# Stage the rebase editor output (edit message 0)
|
||||
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 --
|
||||
edit 0 [user] hello
|
||||
pick 1 [assistant] hi there
|
||||
EDITEOF
|
||||
|
||||
# Stage the edit prompt response
|
||||
cat > .editor_input_1 << 'EDITEOF'
|
||||
# (no previous message)
|
||||
|
||||
Edit message 0 (role: user) below:
|
||||
|
||||
-- >8 --
|
||||
hello world
|
||||
EDITEOF
|
||||
|
||||
# Run drharness with -b flag to edit history
|
||||
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: edit_then_continue - edit phase: expected exit code 0, got $EXIT_CODE"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify the edit was saved
|
||||
msg0_content=$(dr_result chat.json 0)
|
||||
if [ "$msg0_content" != "hello world" ]; then
|
||||
echo "FAIL: edit_then_continue - edit not saved correctly"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Now run drharness normally (without -b) to continue the conversation
|
||||
# We need a new editor input for the normal flow
|
||||
# Reset staged inputs for the second run
|
||||
cat > .editor_input_0 << 'EDITEOF'
|
||||
[assistant] hi there
|
||||
|
||||
-- >8 --
|
||||
|
||||
how are you?
|
||||
EDITEOF
|
||||
|
||||
# Reset the invocation counter for the new run
|
||||
echo "0" > .editor_invocation_count
|
||||
|
||||
# Create mock LLM response for the debug client
|
||||
be_done "I'm doing well, thanks for asking!" > mock_responses.txt
|
||||
|
||||
# Run drharness normally (uses DebugClient to read from stdin)
|
||||
set +e
|
||||
VISUAL="$MOCK_EDITOR" run_drharness -d -F chat.json < mock_responses.txt
|
||||
EXIT_CODE=$?
|
||||
OUTPUT=$(get_output)
|
||||
set -e
|
||||
|
||||
if [ ${EXIT_CODE} -ne 0 ]; then
|
||||
echo "FAIL: edit_then_continue - continue phase: expected exit code 0, got $EXIT_CODE"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify the history now has 4 messages (original 2 + new user message + new assistant response)
|
||||
len=$(history_length chat.json)
|
||||
if [ "$len" != "4" ]; then
|
||||
echo "FAIL: edit_then_continue - expected 4 messages after continue, got $len"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: edit_then_continue"
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_unicode_characters.sh - Unicode characters in message content
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# 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: unicode_characters - expected exit code 0, got $EXIT_CODE"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify history still has 2 messages
|
||||
len=$(history_length chat.json)
|
||||
if [ "$len" != "2" ]; then
|
||||
echo "FAIL: unicode_characters - expected 2 messages, got $len"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify message content preserved unicode
|
||||
msg0_content=$(dr_result chat.json 0)
|
||||
if [ "$msg0_content" != "こんにちは" ]; then
|
||||
echo "FAIL: unicode_characters - message 0 unicode not preserved, got: $msg0_content"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg1_content=$(dr_result chat.json 1)
|
||||
if [ "$msg1_content" != "👋 hello 世界" ]; then
|
||||
echo "FAIL: unicode_characters - message 1 unicode not preserved, got: $msg1_content"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: unicode_characters"
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
# test_edit_history_very_long_content.sh - Very long message content in rebase editor
|
||||
|
||||
# Helper function to run drharness and capture output to a temp file
|
||||
run_drharness() {
|
||||
"$DRHARNESS" "$@" > "$TEST_TEMP_DIR/drharness_output.txt" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Helper to get the output
|
||||
get_output() {
|
||||
cat "$TEST_TEMP_DIR/drharness_output.txt"
|
||||
}
|
||||
|
||||
# Helper to reset editor state for each test
|
||||
reset_editor_state() {
|
||||
rm -f .editor_input_* .editor_invocation_count hist.md
|
||||
}
|
||||
|
||||
reset_editor_state
|
||||
|
||||
# Create a very long message (500 characters)
|
||||
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: very_long_content - expected exit code 0, got $EXIT_CODE"
|
||||
echo "Output: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify history still has 2 messages
|
||||
len=$(history_length chat.json)
|
||||
if [ "$len" != "2" ]; then
|
||||
echo "FAIL: very_long_content - expected 2 messages, got $len"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: very_long_content"
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
# test_edit_prompt_first_message.sh - Test edit first message (no previous message)
|
||||
|
||||
test_name="edit_first_message"
|
||||
|
||||
json=$(create_history_json user "hello" assistant "hi there")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -W "$json" "0" "user")
|
||||
|
||||
# Check for "no previous message" text
|
||||
if ! echo "$OUTPUT" | grep -q "# (no previous message)"; then
|
||||
echo "FAIL: $test_name - should show no previous message"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Should not have "Previous message:" header
|
||||
if echo "$OUTPUT" | grep -q "# Previous message:"; then
|
||||
echo "FAIL: $test_name - should not show previous message header for index 0"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for separator
|
||||
if ! echo "$OUTPUT" | grep -q -- "-- >8 --"; then
|
||||
echo "FAIL: $test_name - missing separator"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for edit prompt
|
||||
if ! echo "$OUTPUT" | grep -q "Edit message 0 (role: user) below:"; then
|
||||
echo "FAIL: $test_name - missing edit prompt"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for current content
|
||||
if ! echo "$OUTPUT" | grep -q "hello"; then
|
||||
echo "FAIL: $test_name - missing current content"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
# test_edit_prompt_middle_message.sh - Test edit middle message shows only immediate previous as context
|
||||
|
||||
test_name="edit_middle_message"
|
||||
|
||||
json=$(create_history_json user "msg0" assistant "msg1" tool "msg2")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -W "$json" "2" "tool")
|
||||
|
||||
# Should show message 1 as context (immediate previous)
|
||||
if ! echo "$OUTPUT" | grep -q "\[1\] assistant: msg1"; then
|
||||
echo "FAIL: $test_name - should show message 1 as context"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Should NOT show message 0 in context
|
||||
if echo "$OUTPUT" | grep -q "\[0\]"; then
|
||||
echo "FAIL: $test_name - should not show message 0 in context"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Should show edit prompt for message 2
|
||||
if ! echo "$OUTPUT" | grep -q "Edit message 2 (role: tool) below:"; then
|
||||
echo "FAIL: $test_name - should show edit prompt for message 2"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Should show current content (msg2)
|
||||
if ! echo "$OUTPUT" | grep -q "msg2"; then
|
||||
echo "FAIL: $test_name - should show current content"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
# test_edit_prompt_multiline_context.sh - Test multi-line content in context shows full content
|
||||
|
||||
test_name="multiline_context"
|
||||
|
||||
json=$(create_history_json user $'line one\nline two' assistant "response")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -W "$json" "1" "assistant")
|
||||
|
||||
# Both lines should appear in context
|
||||
if ! echo "$OUTPUT" | grep -q "line one"; then
|
||||
echo "FAIL: $test_name - first line of context missing"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "line two"; then
|
||||
echo "FAIL: $test_name - second line of context missing"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# test_edit_prompt_separator_placement.sh - Test separator correctly placed after edit prompt
|
||||
|
||||
test_name="separator_placement"
|
||||
|
||||
json=$(create_history_json user "first" assistant "second")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -W "$json" "1" "assistant")
|
||||
|
||||
# The separator should appear after the context and edit prompt
|
||||
# Check that "Edit message" is followed by the separator
|
||||
if ! echo "$OUTPUT" | grep -A2 "Edit message 1" | grep -q -- "-- >8 --"; then
|
||||
echo "FAIL: $test_name - separator not correctly placed after edit prompt"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# test_edit_prompt_tool_role.sh - Test edit with tool role
|
||||
|
||||
test_name="edit_tool_role"
|
||||
|
||||
json=$(create_history_json assistant "call tool" tool "result")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -W "$json" "1" "tool")
|
||||
|
||||
# Check for tool role in edit prompt
|
||||
if ! echo "$OUTPUT" | grep -q "Edit message 1 (role: tool) below:"; then
|
||||
echo "FAIL: $test_name - should show tool role in edit prompt"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
# test_edit_prompt_with_context.sh - Test edit message with previous message context
|
||||
|
||||
test_name="edit_with_context"
|
||||
|
||||
# Two messages: editing message 1, so message 0 should be shown as context
|
||||
json=$(create_history_json user "hello" assistant "hi there")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -W "$json" "1" "assistant")
|
||||
|
||||
# Check for previous message context
|
||||
if ! echo "$OUTPUT" | grep -q "# Previous message:"; then
|
||||
echo "FAIL: $test_name - missing previous message header"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for index 0 in context
|
||||
if ! echo "$OUTPUT" | grep -q "\[0\] user: hello"; then
|
||||
echo "FAIL: $test_name - missing context message with index"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for separator
|
||||
if ! echo "$OUTPUT" | grep -q -- "-- >8 --"; then
|
||||
echo "FAIL: $test_name - missing separator"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for edit prompt
|
||||
if ! echo "$OUTPUT" | grep -q "Edit message 1 (role: assistant) below:"; then
|
||||
echo "FAIL: $test_name - missing edit prompt"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for current content
|
||||
if ! echo "$OUTPUT" | grep -q "hi there"; then
|
||||
echo "FAIL: $test_name - missing current content"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
@@ -3,43 +3,64 @@
|
||||
|
||||
# Test parse_json_safely with valid JSON
|
||||
OUTPUT=$("${DRHARNESS}" -J '{"key": "value"}')
|
||||
if [ "${OUTPUT}" != "valid" ]; then
|
||||
echo "FAIL: parse_json_safely - expected 'valid' for valid JSON, got '${OUTPUT}'"
|
||||
if ! echo "$OUTPUT" | jq -e '.key == "value"' > /dev/null 2>&1; then
|
||||
echo "FAIL: parse_json_safely - expected valid JSON with key='value', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test parse_json_safely with valid JSON object (empty)
|
||||
OUTPUT=$("${DRHARNESS}" -J '{}')
|
||||
if [ "${OUTPUT}" != "valid" ]; then
|
||||
echo "FAIL: parse_json_safely - expected 'valid' for empty object, got '${OUTPUT}'"
|
||||
if ! echo "$OUTPUT" | jq -e 'type == "object"' > /dev/null 2>&1; then
|
||||
echo "FAIL: parse_json_safely - expected valid empty object, got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test parse_json_safely with valid JSON array
|
||||
OUTPUT=$("${DRHARNESS}" -J '[1, 2, 3]')
|
||||
if [ "${OUTPUT}" != "valid" ]; then
|
||||
echo "FAIL: parse_json_safely - expected 'valid' for array, got '${OUTPUT}'"
|
||||
if ! echo "$OUTPUT" | jq -e 'type == "array" and length == 3' > /dev/null 2>&1; then
|
||||
echo "FAIL: parse_json_safely - expected valid array, got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test parse_json_safely with invalid JSON (missing quotes)
|
||||
OUTPUT=$("${DRHARNESS}" -J '{key: value}')
|
||||
if [ "${OUTPUT}" != "discarded" ]; then
|
||||
echo "FAIL: parse_json_safely - expected 'discarded' for invalid JSON, got '${OUTPUT}'"
|
||||
set +e
|
||||
OUTPUT=$("${DRHARNESS}" -J '{key: value}' 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
if [ ${EXIT_CODE} -eq 0 ]; then
|
||||
echo "FAIL: parse_json_safely - expected non-zero exit code for invalid JSON, got 0"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$OUTPUT" | grep -q "error: invalid JSON"; then
|
||||
echo "FAIL: parse_json_safely - expected 'error: invalid JSON' for invalid JSON, got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test parse_json_safely with invalid JSON (random text)
|
||||
OUTPUT=$("${DRHARNESS}" -J 'not json at all')
|
||||
if [ "${OUTPUT}" != "discarded" ]; then
|
||||
echo "FAIL: parse_json_safely - expected 'discarded' for random text, got '${OUTPUT}'"
|
||||
set +e
|
||||
OUTPUT=$("${DRHARNESS}" -J 'not json at all' 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
if [ ${EXIT_CODE} -eq 0 ]; then
|
||||
echo "FAIL: parse_json_safely - expected non-zero exit code for invalid JSON, got 0"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$OUTPUT" | grep -q "error: invalid JSON"; then
|
||||
echo "FAIL: parse_json_safely - expected 'error: invalid JSON' for random text, got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test parse_json_safely with empty string
|
||||
OUTPUT=$("${DRHARNESS}" -J '')
|
||||
if [ "${OUTPUT}" != "discarded" ]; then
|
||||
echo "FAIL: parse_json_safely - expected 'discarded' for empty string, got '${OUTPUT}'"
|
||||
set +e
|
||||
OUTPUT=$("${DRHARNESS}" -J '' 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
if [ ${EXIT_CODE} -eq 0 ]; then
|
||||
echo "FAIL: parse_json_safely - expected non-zero exit code for empty string, got 0"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$OUTPUT" | grep -q "error: invalid JSON"; then
|
||||
echo "FAIL: parse_json_safely - expected 'error: invalid JSON' for empty string, got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_apply_all_lines_deleted_keeps.sh - All lines deleted (only header) keeps all messages
|
||||
|
||||
# Empty commands - simulates deleting all message lines
|
||||
lines='[]'
|
||||
history=$(create_messages_json user "item 0" assistant "item 1")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -A "$lines" "$history")
|
||||
|
||||
# Should have 2 messages (all kept because no commands specified)
|
||||
msg_count=$(echo "$OUTPUT" | grep -o '"role"' | wc -l)
|
||||
|
||||
if [ "$msg_count" -ne 2 ]; then
|
||||
echo "FAIL: all_lines_deleted_keeps_all - expected 2 messages, got $msg_count"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: all_lines_deleted_keeps_all"
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_apply_deleting_lines_drops.sh - Deleting lines from rebase editor drops those messages (issue 120)
|
||||
|
||||
# Simulate editing a 6-item history where items 4 and 5 are deleted
|
||||
# Only items 0-3 are mentioned in the commands
|
||||
lines=$(create_rebase_lines pick 0 user pick 1 assistant pick 2 tool pick 3 assistant)
|
||||
history=$(create_messages_json \
|
||||
user "item 0" assistant "item 1" tool "item 2" assistant "item 3" \
|
||||
user "item 4" assistant "item 5")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -A "$lines" "$history")
|
||||
|
||||
# Should have 4 messages (items 4 and 5 dropped)
|
||||
msg_count=$(echo "$OUTPUT" | grep -o '"role"' | wc -l)
|
||||
|
||||
if [ "$msg_count" -ne 4 ]; then
|
||||
echo "FAIL: deleting_lines_drops_messages - expected 4 messages, got $msg_count"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Should not contain item 4 or item 5
|
||||
if echo "$OUTPUT" | grep -q '"item 4"'; then
|
||||
echo "FAIL: deleting_lines_drops_messages - item 4 should be dropped"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if echo "$OUTPUT" | grep -q '"item 5"'; then
|
||||
echo "FAIL: deleting_lines_drops_messages - item 5 should be dropped"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: deleting_lines_drops_messages"
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_apply_drop_removes.sh - Drop commands remove messages from result
|
||||
|
||||
lines=$(create_rebase_lines drop 0 user pick 1 assistant)
|
||||
history=$(create_messages_json user "hello" assistant "hi there")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -A "$lines" "$history")
|
||||
|
||||
# Should only have one message (the assistant response)
|
||||
msg_count=$(echo "$OUTPUT" | grep -o '"role"' | wc -l)
|
||||
|
||||
if [ "$msg_count" -ne 1 ]; then
|
||||
echo "FAIL: drop_removes_message - expected 1 message, got $msg_count"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Should not contain the dropped message content
|
||||
if echo "$OUTPUT" | grep -q '"hello"'; then
|
||||
echo "FAIL: drop_removes_message - dropped message should not be in output"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: drop_removes_message"
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_apply_edit_preserves_fields.sh - Non-edited fields are preserved during edits
|
||||
|
||||
lines=$(create_rebase_lines edit 0 assistant)
|
||||
history=$(create_messages_json user "hello" reasoning "thought process" tool_call_id "call_123")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -A "$lines" "$history")
|
||||
|
||||
# Check reasoning is preserved
|
||||
if ! echo "$OUTPUT" | grep -q '"thought process"'; then
|
||||
echo "FAIL: edit_preserves_fields - reasoning should be preserved"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check tool_call_id is preserved
|
||||
if ! echo "$OUTPUT" | grep -q '"call_123"'; then
|
||||
echo "FAIL: edit_preserves_fields - tool_call_id should be preserved"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: edit_preserves_fields"
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_apply_edit_preserves_tool_calls.sh - tool_calls field preserved during edit
|
||||
|
||||
lines=$(create_rebase_lines edit 0 assistant)
|
||||
history=$(create_messages_json assistant "calling tool" tool_call "call_1" "test" "{}")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -A "$lines" "$history")
|
||||
|
||||
# tool_calls should be preserved
|
||||
if ! echo "$OUTPUT" | grep -q '"tool_calls"'; then
|
||||
echo "FAIL: edit_preserves_tool_calls - tool_calls should be preserved during edit"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: edit_preserves_tool_calls"
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_apply_edit_updates_role.sh - Edit commands update role
|
||||
|
||||
lines=$(create_rebase_lines edit 0 assistant)
|
||||
history=$(create_messages_json user "hello world")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -A "$lines" "$history")
|
||||
|
||||
# Check role was changed to assistant
|
||||
if ! echo "$OUTPUT" | grep -q '"role".*"assistant"'; then
|
||||
echo "FAIL: edit_updates_role - role should be changed to assistant"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Content should still be preserved
|
||||
if ! echo "$OUTPUT" | grep -q '"hello world"'; then
|
||||
echo "FAIL: edit_updates_role - content should be preserved during edit"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: edit_updates_role"
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_apply_empty_lines_preserves.sh - Empty lines array preserves all messages
|
||||
|
||||
lines='[]'
|
||||
history=$(create_messages_json user "hello" assistant "hi")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -A "$lines" "$history")
|
||||
|
||||
# Should have 2 messages (all implicitly picked)
|
||||
msg_count=$(echo "$OUTPUT" | grep -o '"role"' | wc -l)
|
||||
|
||||
if [ "$msg_count" -ne 2 ]; then
|
||||
echo "FAIL: empty_lines_preserves_all - expected 2 messages, got $msg_count"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: empty_lines_preserves_all"
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_apply_implicit_pick.sh - Messages not referenced by any command are dropped (implicit drop)
|
||||
|
||||
# Only specify command for index 0, index 1 should be implicitly dropped
|
||||
lines=$(create_rebase_lines pick 0 user)
|
||||
history=$(create_messages_json user "hello" assistant "hi there")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -A "$lines" "$history")
|
||||
|
||||
# Should have 1 message (index 1 dropped)
|
||||
msg_count=$(echo "$OUTPUT" | grep -o '"role"' | wc -l)
|
||||
|
||||
if [ "$msg_count" -ne 1 ]; then
|
||||
echo "FAIL: implicit_pick - expected 1 message, got $msg_count"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: implicit_pick"
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_apply_lenient_orphaned_results.sh - Lenient mode - orphaned tool results are allowed
|
||||
|
||||
# Drop the tool call message but keep the result (orphaned)
|
||||
lines=$(create_rebase_lines drop 0 assistant pick 1 tool)
|
||||
history=$(create_messages_json \
|
||||
assistant "" tool_call "call_1" "test" "" \
|
||||
tool "result" tool_call_id "call_1")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -A "$lines" "$history")
|
||||
|
||||
# Should have 1 message (the orphaned tool result)
|
||||
msg_count=$(echo "$OUTPUT" | grep -o '"role"' | wc -l)
|
||||
|
||||
if [ "$msg_count" -ne 1 ]; then
|
||||
echo "FAIL: lenient_orphaned_results - expected 1 message (orphaned result), got $msg_count"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The orphaned tool result should be present
|
||||
if ! echo "$OUTPUT" | grep -q '"tool"'; then
|
||||
echo "FAIL: lenient_orphaned_results - orphaned tool result should be present"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: lenient_orphaned_results"
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_apply_mixed_commands.sh - Mixed commands (pick, drop, edit)
|
||||
|
||||
lines=$(create_rebase_lines pick 0 user drop 1 assistant edit 2 tool)
|
||||
history=$(create_messages_json user "hello" assistant "hi" tool "result")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -A "$lines" "$history")
|
||||
|
||||
# Should have 2 messages (index 1 dropped)
|
||||
msg_count=$(echo "$OUTPUT" | grep -o '"role"' | wc -l)
|
||||
|
||||
if [ "$msg_count" -ne 2 ]; then
|
||||
echo "FAIL: mixed_commands - expected 2 messages, got $msg_count"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: mixed_commands"
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_apply_pick_preserves.sh - Pick commands preserve messages unchanged
|
||||
|
||||
lines=$(create_rebase_lines pick 0 user)
|
||||
history=$(create_messages_json user "hello world")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -A "$lines" "$history")
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q '"hello world"'; then
|
||||
echo "FAIL: pick_preserves_message - content should be preserved"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q '"role".*"user"'; then
|
||||
echo "FAIL: pick_preserves_message - role should be preserved"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check we got 1 message back
|
||||
if ! echo "$OUTPUT" | grep -q '"role"'; then
|
||||
echo "FAIL: pick_preserves_message - expected output"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: pick_preserves_message"
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_content_basic_messages.sh - Test basic messages in rebase content
|
||||
|
||||
test_name="basic_messages"
|
||||
|
||||
json=$(create_messages_json user "hello" assistant "hi there")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$json")
|
||||
|
||||
# Check header exists
|
||||
if ! echo "$OUTPUT" | grep -q "# History Rebase Editor"; then
|
||||
echo "FAIL: $test_name - missing header"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check pick commands
|
||||
if ! echo "$OUTPUT" | grep -q "pick 0 \[user\] hello"; then
|
||||
echo "FAIL: $test_name - missing pick 0"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "pick 1 \[assistant\] hi there"; then
|
||||
echo "FAIL: $test_name - missing pick 1"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_content_empty_message.sh - Test empty message shows (empty)
|
||||
|
||||
test_name="empty_message"
|
||||
|
||||
json=$(create_messages_json user "")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$json")
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "pick 0 \[user\] (empty)"; then
|
||||
echo "FAIL: $test_name - should show (empty) for empty content"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_content_header_comments.sh - Test header explains pick/edit/drop commands
|
||||
|
||||
test_name="header_comments"
|
||||
|
||||
json=$(create_messages_json user "test")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$json")
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "pick - keep the message unchanged"; then
|
||||
echo "FAIL: $test_name - missing pick explanation"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "edit - modify the message content and/or role"; then
|
||||
echo "FAIL: $test_name - missing edit explanation"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "drop - remove the message from history"; then
|
||||
echo "FAIL: $test_name - missing drop explanation"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_content_message_indices.sh - Test each message on its own line with correct index
|
||||
|
||||
test_name="message_indices"
|
||||
|
||||
json=$(create_messages_json user "msg1" assistant "msg2" tool "msg3")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$json")
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "pick 0 \[user\] msg1"; then
|
||||
echo "FAIL: $test_name - missing index 0"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "pick 1 \[assistant\] msg2"; then
|
||||
echo "FAIL: $test_name - missing index 1"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "pick 2 \[tool\] msg3"; then
|
||||
echo "FAIL: $test_name - missing index 2"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_content_multiline_content.sh - Test multi-line content shows only first line
|
||||
|
||||
test_name="multiline_content"
|
||||
|
||||
json=$(create_messages_json user "first line
|
||||
second line
|
||||
third line")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$json")
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "pick 0 \[user\] first line"; then
|
||||
echo "FAIL: $test_name - should show only first line"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make sure it doesn't include the other lines
|
||||
if echo "$OUTPUT" | grep -q "second line"; then
|
||||
echo "FAIL: $test_name - should not include second line in pick command"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_content_multiple_tool_calls.sh - Test multiple tool calls shows first one with ...
|
||||
|
||||
test_name="multiple_tool_calls"
|
||||
|
||||
json=$(create_messages_json assistant "" tool_call "call_1" "read_file" "" tool_call "call_2" "write_file" "")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$json")
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "pick 0 \[assistant\] \[tool_call: read_file \.\.\.\]"; then
|
||||
echo "FAIL: $test_name - should show first tool call with ..."
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_content_scissors_line.sh - Test scissors line separates comments from commands
|
||||
|
||||
test_name="scissors_line"
|
||||
|
||||
json=$(create_messages_json user "test")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$json")
|
||||
|
||||
# Check for scissors line
|
||||
if ! echo "$OUTPUT" | grep -q -- "-- >8 --"; then
|
||||
echo "FAIL: $test_name - missing scissors line"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check that scissors line appears before pick commands
|
||||
scissors_line=$(echo "$OUTPUT" | grep -n -- "-- >8 --" | head -1 | cut -d: -f1)
|
||||
pick_line=$(echo "$OUTPUT" | grep -n "^pick" | head -1 | cut -d: -f1)
|
||||
|
||||
if [ "$scissors_line" -ge "$pick_line" ]; then
|
||||
echo "FAIL: $test_name - scissors line should appear before pick commands"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_content_special_characters.sh - Test special characters in content
|
||||
|
||||
test_name="special_characters"
|
||||
|
||||
json=$(create_messages_json user 'hello "quotes" and $dollar' assistant 'backslash\here')
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$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"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_content_tool_call_display.sh - Test assistant message with tool_calls display
|
||||
|
||||
test_name="tool_call_display"
|
||||
|
||||
json=$(create_messages_json assistant "" tool_call "call_1" "run_shell" "")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$json")
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "pick 0 \[assistant\] \[tool_call: run_shell\]"; then
|
||||
echo "FAIL: $test_name - should show tool_call display"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_content_tool_message.sh - Test tool message shows result content
|
||||
|
||||
test_name="tool_message"
|
||||
|
||||
json=$(create_messages_json tool "tool result content")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$json")
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "pick 0 \[tool\] tool result content"; then
|
||||
echo "FAIL: $test_name - should show tool result content"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_content_unicode_characters.sh - Test unicode characters in content
|
||||
|
||||
test_name="unicode_characters"
|
||||
|
||||
json=$(create_messages_json user "こんにちは" assistant "👋 hello 世界")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$json")
|
||||
|
||||
# Should preserve unicode characters
|
||||
if ! echo "$OUTPUT" | grep -q "pick 0 \[user\] こんにちは"; then
|
||||
echo "FAIL: $test_name - should preserve unicode"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 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"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_content_very_long_content.sh - Test very long content (truncation test)
|
||||
|
||||
test_name="very_long_content"
|
||||
|
||||
# Create a very long string (1000 characters)
|
||||
long_content=$(printf 'x%.0s' {1..1000})
|
||||
json=$(create_messages_json user "$long_content")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$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"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_parser_all_commands.sh - Valid rebase file with all three commands
|
||||
|
||||
test_name="valid_rebase_all_commands"
|
||||
|
||||
content="pick 0 [user] hello world
|
||||
edit 1 [assistant] ai response
|
||||
drop 2 [tool] tool result"
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -z 3 "$content")
|
||||
|
||||
count=$(rebase_line_count "$OUTPUT")
|
||||
if [ "$count" -ne 3 ]; then
|
||||
echo "FAIL: $test_name - expected 3 lines, got $count"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$(rebase_command_at "$OUTPUT" 0)" != "pick" ]; then
|
||||
echo "FAIL: $test_name - expected pick at 0"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(rebase_index_at "$OUTPUT" 0)" != "0" ]; then
|
||||
echo "FAIL: $test_name - expected index 0 at 0"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(rebase_role_at "$OUTPUT" 0)" != "user" ]; then
|
||||
echo "FAIL: $test_name - expected role user at 0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$(rebase_command_at "$OUTPUT" 1)" != "edit" ]; then
|
||||
echo "FAIL: $test_name - expected edit at 1"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(rebase_command_at "$OUTPUT" 2)" != "drop" ]; then
|
||||
echo "FAIL: $test_name - expected drop at 2"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_parser_all_valid_roles.sh - All three valid roles accepted
|
||||
|
||||
test_name="all_valid_roles"
|
||||
|
||||
content="pick 0 [user] hello
|
||||
pick 1 [assistant] response
|
||||
pick 2 [tool] result"
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -z 3 "$content")
|
||||
|
||||
if [ "$(rebase_role_at "$OUTPUT" 0)" != "user" ]; then
|
||||
echo "FAIL: $test_name - expected user role at 0"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(rebase_role_at "$OUTPUT" 1)" != "assistant" ]; then
|
||||
echo "FAIL: $test_name - expected assistant role at 1"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(rebase_role_at "$OUTPUT" 2)" != "tool" ]; then
|
||||
echo "FAIL: $test_name - expected tool role at 2"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_parser_comments_and_empty_lines_mixed.sh - Comments and empty lines mixed with commands
|
||||
|
||||
test_name="comments_and_empty_lines_mixed"
|
||||
|
||||
content="# Header comment
|
||||
pick 0 [user] hello
|
||||
|
||||
# Middle comment
|
||||
edit 1 [assistant] response
|
||||
|
||||
drop 2 [tool] result
|
||||
# Footer comment"
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -z 3 "$content")
|
||||
|
||||
count=$(rebase_line_count "$OUTPUT")
|
||||
if [ "$count" -ne 3 ]; then
|
||||
echo "FAIL: $test_name - expected 3 lines, got $count"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_parser_duplicate_indices.sh - Duplicate indices produces error
|
||||
|
||||
test_name="duplicate_indices"
|
||||
|
||||
content="pick 0 [user] hello
|
||||
pick 1 [assistant] response
|
||||
drop 0 [tool] duplicate"
|
||||
|
||||
set +e
|
||||
OUTPUT=$("${DRHARNESS}" -z 3 "$content" 2>&1) || true
|
||||
set -e
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "duplicate index 0"; then
|
||||
echo "FAIL: $test_name - expected 'duplicate index 0' error"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_parser_empty_rebase.sh - Empty rebase file is valid (no lines)
|
||||
|
||||
test_name="empty_rebase"
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -z 3 "")
|
||||
|
||||
count=$(rebase_line_count "$OUTPUT")
|
||||
if [ "$count" -ne 0 ]; then
|
||||
echo "FAIL: $test_name - expected 0 lines, got $count"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_parser_first_error_returned.sh - First error is returned (not all errors aggregated)
|
||||
|
||||
test_name="first_error_returned"
|
||||
|
||||
content="invalid 0 [user] bad command
|
||||
pick 10 [assistant] out of range
|
||||
drop 100 [tool] also out of range"
|
||||
|
||||
set +e
|
||||
OUTPUT=$("${DRHARNESS}" -z 3 "$content" 2>&1) || true
|
||||
set -e
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "invalid command: 'invalid'"; then
|
||||
echo "FAIL: $test_name - expected 'invalid command' as first error"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Should NOT see the other errors since we exit on first error
|
||||
if echo "$OUTPUT" | grep -q "index 10 out of range"; then
|
||||
echo "FAIL: $test_name - should not see second error (exit on first)"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if echo "$OUTPUT" | grep -q "index 100 out of range"; then
|
||||
echo "FAIL: $test_name - should not see third error (exit on first)"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_parser_index_out_of_range.sh - Out of range index produces error
|
||||
|
||||
test_name="index_out_of_range"
|
||||
|
||||
content="pick 0 [user] hello
|
||||
pick 10 [assistant] response"
|
||||
|
||||
set +e
|
||||
OUTPUT=$("${DRHARNESS}" -z 3 "$content" 2>&1) || true
|
||||
set -e
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "index 10 out of range"; then
|
||||
echo "FAIL: $test_name - expected 'index 10 out of range' error"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_parser_invalid_command.sh - Invalid command produces error
|
||||
|
||||
test_name="invalid_command"
|
||||
|
||||
content="pick 0 [user] hello
|
||||
squash 1 [assistant] response"
|
||||
|
||||
set +e
|
||||
OUTPUT=$("${DRHARNESS}" -z 3 "$content" 2>&1) || true
|
||||
set -e
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "invalid command: 'squash'"; then
|
||||
echo "FAIL: $test_name - expected 'invalid command: squash' error"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_parser_invalid_command_digit.sh - Line starting with digit (invalid command) produces error
|
||||
|
||||
test_name="invalid_command_digit"
|
||||
|
||||
content="0 [user] no command"
|
||||
|
||||
set +e
|
||||
OUTPUT=$("${DRHARNESS}" -z 3 "$content" 2>&1) || true
|
||||
set -e
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "invalid command: '0'"; then
|
||||
echo "FAIL: $test_name - expected 'invalid command: 0' error"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_parser_invalid_index.sh - Invalid index produces error
|
||||
|
||||
test_name="invalid_index"
|
||||
|
||||
content="pick [user] no valid index"
|
||||
|
||||
set +e
|
||||
OUTPUT=$("${DRHARNESS}" -z 3 "$content" 2>&1) || true
|
||||
set -e
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "invalid index: '\[user\]'"; then
|
||||
echo "FAIL: $test_name - expected 'invalid index' error"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_parser_invalid_role_value.sh - Invalid role value produces error
|
||||
|
||||
test_name="invalid_role_value"
|
||||
|
||||
content="pick 0 [user] hello
|
||||
pick 1 [system] response"
|
||||
|
||||
set +e
|
||||
OUTPUT=$("${DRHARNESS}" -z 3 "$content" 2>&1) || true
|
||||
set -e
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "invalid role: '\[system\]'"; then
|
||||
echo "FAIL: $test_name - expected 'invalid role' error"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_parser_malformed_role.sh - Missing role brackets produces error
|
||||
|
||||
test_name="malformed_role"
|
||||
|
||||
content="pick 0 [user] hello
|
||||
pick 1 assistant response"
|
||||
|
||||
set +e
|
||||
OUTPUT=$("${DRHARNESS}" -z 3 "$content" 2>&1) || true
|
||||
set -e
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "malformed role: 'assistant'"; then
|
||||
echo "FAIL: $test_name - expected 'malformed role' error"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_parser_missing_role.sh - Missing role produces error
|
||||
|
||||
test_name="missing_role"
|
||||
|
||||
content="pick 0"
|
||||
|
||||
set +e
|
||||
OUTPUT=$("${DRHARNESS}" -z 3 "$content" 2>&1) || true
|
||||
set -e
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "missing role"; then
|
||||
echo "FAIL: $test_name - expected 'missing role' error"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_parser_only_comments.sh - Only comments produces 0 lines
|
||||
|
||||
test_name="only_comments"
|
||||
|
||||
content="# comment 1
|
||||
# comment 2"
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -z 3 "$content")
|
||||
|
||||
count=$(rebase_line_count "$OUTPUT")
|
||||
if [ "$count" -ne 0 ]; then
|
||||
echo "FAIL: $test_name - expected 0 lines, got $count"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_parser_whitespace_variations.sh - Pick commands with whitespace variations
|
||||
|
||||
test_name="whitespace_variations"
|
||||
|
||||
# Test with multiple spaces between fields
|
||||
content="pick 0 [user] hello
|
||||
edit 1 [assistant] response
|
||||
drop 2 [tool] result"
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -z 3 "$content")
|
||||
|
||||
count=$(rebase_line_count "$OUTPUT")
|
||||
if [ "$count" -ne 3 ]; then
|
||||
echo "FAIL: $test_name - expected 3 lines, got $count"
|
||||
echo "Got: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_parser_with_comments.sh - Valid rebase with comments ignored
|
||||
|
||||
test_name="valid_rebase_with_comments"
|
||||
|
||||
content="# This is a comment
|
||||
pick 0 [user] hello
|
||||
# Another comment
|
||||
edit 1 [assistant] response"
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -z 3 "$content")
|
||||
|
||||
count=$(rebase_line_count "$OUTPUT")
|
||||
if [ "$count" -ne 2 ]; then
|
||||
echo "FAIL: $test_name - expected 2 lines, got $count"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# test_rebase_parser_with_empty_lines.sh - Valid rebase with empty lines
|
||||
|
||||
test_name="valid_rebase_with_empty_lines"
|
||||
|
||||
content="pick 0 [user] hello
|
||||
|
||||
edit 1 [assistant] response
|
||||
|
||||
drop 2 [tool] result"
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -z 3 "$content")
|
||||
|
||||
count=$(rebase_line_count "$OUTPUT")
|
||||
if [ "$count" -ne 3 ]; then
|
||||
echo "FAIL: $test_name - expected 3 lines, got $count"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: $test_name"
|
||||
+1
-1
@@ -35,7 +35,7 @@ Tool make_edit_tool() {
|
||||
{"properties", {
|
||||
{"filePath", {
|
||||
{"type", "string"},
|
||||
{"description", "The absolute path to the file to modify"}
|
||||
{"description", "The path to the file to modify (relative or absolute)"}
|
||||
}},
|
||||
{"oldString", {
|
||||
{"type", "string"},
|
||||
|
||||
@@ -12,7 +12,7 @@ Tool make_file_list_tool() {
|
||||
{"properties", {
|
||||
{"path", {
|
||||
{"type", "string"},
|
||||
{"description", "The absolute path to the directory to list."}
|
||||
{"description", "The path to the directory to list (relative or absolute)."}
|
||||
}},
|
||||
{"recursive", {
|
||||
{"type", "boolean"},
|
||||
|
||||
@@ -14,7 +14,7 @@ Tool make_file_read_tool() {
|
||||
{"properties", {
|
||||
{"path", {
|
||||
{"type", "string"},
|
||||
{"description", "The absolute path to the file to read."}
|
||||
{"description", "The path to the file to read (relative or absolute)."}
|
||||
}}
|
||||
}},
|
||||
{"required", {"path"}}
|
||||
|
||||
Reference in New Issue
Block a user