Merge origin/master into work branch (issue #169)
Resolved merge conflicts: - CMakeLists.txt: Added both rebase_engine/utility and base64_util sources - src/harness.cpp: Combined Content type with proper error handling - src/main.cpp: Merged includes, test flags, and CLI parsing - Renamed conflicting flags: -B→-Z (build_rebase), -T→-W (edit_prompt), -b→-K (base64_decode) - tests/CMakeLists.txt: Added all test targets from both branches - src/rebase_engine.cpp: Fixed to use get_content_string() for Content variant type - Updated test files to use new flag names (-Z, -W, -K)
This commit is contained in:
+3
-1
@@ -12,6 +12,7 @@ set(TOOL_SOURCES
|
||||
tools/shell_tool.cpp
|
||||
tools/agent_tool.cpp
|
||||
tools/edit_tool.cpp
|
||||
tools/read_multi_modal.cpp
|
||||
tools/file_list_tool.cpp
|
||||
tools/file_read_tool.cpp
|
||||
tools/grep_tool.cpp
|
||||
@@ -30,8 +31,9 @@ add_library(drharness_lib
|
||||
src/debug_client.cpp
|
||||
src/editor.cpp
|
||||
src/skill.cpp
|
||||
src/rebase_engine.cpp
|
||||
src/rebase_engine.cpp
|
||||
src/utility.cpp
|
||||
src/base64_util.cpp
|
||||
${TOOL_SOURCES}
|
||||
)
|
||||
target_include_directories(drharness_lib PUBLIC include tools)
|
||||
|
||||
@@ -11,6 +11,9 @@ RUN apk add nlohmann-json
|
||||
RUN apk add cmake
|
||||
RUN apk add jq
|
||||
RUN apk add bash
|
||||
RUN apk add git
|
||||
RUN git config --global user.email "hal9000@example.com"
|
||||
RUN git config --global user.name "hal9000"
|
||||
COPY . /drharness
|
||||
RUN mkdir -p /usr/local/share/ca-certificates/
|
||||
RUN cp /drharness/192.168.8.63.crt /usr/local/share/ca-certificates/192.168.8.63.crt
|
||||
|
||||
@@ -73,6 +73,7 @@ configuration.
|
||||
* `-p <file>`: Pretty-print a conversation JSON file without running the harness.
|
||||
* `-t <tool>`: Manually constrain the harness to a specific subset of tools (by default no tools are provided to the model)
|
||||
* `-s <name>`: Manually constrain the harness to a specific subset of skills (can be specified multiple times)
|
||||
* `-i <path>`: Image file to attach to the prompt (can be specified multiple times)
|
||||
* `-m <prompt>`: The user prompt to send to the agent.
|
||||
* `-M <model>`: Override the Ollama model (e.g., `mistral`, `llama3`).
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <expected>
|
||||
#include <vector>
|
||||
|
||||
// Base64 encode binary data to base64 string
|
||||
std::string base64_encode(const std::string& input);
|
||||
|
||||
// Base64 decode base64 string to binary data
|
||||
std::expected<std::string, std::string> base64_decode(const std::string& input);
|
||||
|
||||
// Get MIME type from file extension
|
||||
std::string get_mime_type_from_extension(const std::string& extension);
|
||||
|
||||
// Generate data URI prefix with base64 data
|
||||
std::string generate_data_uri(const std::string& extension, const std::string& base64_data);
|
||||
|
||||
// Read an image file and return it as a base64 data URI
|
||||
std::expected<std::string, std::string> read_image_as_data_uri(const std::string& file_path);
|
||||
@@ -15,6 +15,8 @@ public:
|
||||
HarnessState deserialized);
|
||||
|
||||
std::expected<std::string, std::string> chat(const std::string& user_message);
|
||||
std::expected<std::string, std::string> chat(const Message& user_message);
|
||||
std::expected<std::string, std::string> chat(const std::string& user_message, const std::vector<std::string>& image_paths);
|
||||
void reset();
|
||||
|
||||
HarnessState serialize() const;
|
||||
|
||||
+226
-2
@@ -3,12 +3,14 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <iostream>
|
||||
#include <cstdlib>
|
||||
#include <regex>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <functional>
|
||||
#include <variant>
|
||||
|
||||
// Forward declaration of template function for JSON deserialization
|
||||
// Specialized implementations are generated by DRH_DEFINE_TYPE_* macros
|
||||
@@ -29,6 +31,10 @@ inline constexpr bool has_json_to_v = has_json_to<T>::value;
|
||||
template<typename T>
|
||||
struct has_json_to<std::vector<T>, std::enable_if_t<has_json_to_v<T>>> : std::true_type {};
|
||||
|
||||
// Partial specialization for std::variant - always true since json_to_impl handles it
|
||||
template<typename... Alternatives>
|
||||
struct has_json_to<std::variant<Alternatives...>> : std::true_type {};
|
||||
|
||||
// Helper function for json_to - uses if constexpr to dispatch between
|
||||
// recursive json_to<T>() calls (for custom types) and direct .get_to() calls (for primitives)
|
||||
template<typename T>
|
||||
@@ -88,6 +94,122 @@ std::expected<T, std::string> json_to(const nlohmann::json& j) {
|
||||
return json_to_impl<T>::apply(j);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// std::optional serialization/deserialization support
|
||||
// ============================================================================
|
||||
|
||||
// Partial specialization for std::optional<T> - always true since we handle it
|
||||
template<typename T>
|
||||
struct has_json_to<std::optional<T>> : std::true_type {};
|
||||
|
||||
// to_json for std::optional - serializes as null when empty, value when present
|
||||
template<typename T>
|
||||
void to_json(nlohmann::json& j, const std::optional<T>& opt) {
|
||||
if (opt.has_value()) {
|
||||
j = opt.value();
|
||||
} else {
|
||||
j = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// json_to_impl specialization for std::optional<T>
|
||||
// Deserializes null as std::nullopt, otherwise deserializes the inner type
|
||||
template<typename T>
|
||||
struct json_to_impl<std::optional<T>> {
|
||||
static std::expected<std::optional<T>, std::string> apply(const nlohmann::json& j) {
|
||||
if (j.is_null()) {
|
||||
return std::optional<T>{};
|
||||
}
|
||||
if constexpr (has_json_to_v<T>) {
|
||||
auto result = json_to<T>(j);
|
||||
if (!result) {
|
||||
return std::unexpected(result.error());
|
||||
}
|
||||
return std::optional<T>{std::move(result.value())};
|
||||
} else {
|
||||
try {
|
||||
T value;
|
||||
j.get_to(value);
|
||||
return std::optional<T>{std::move(value)};
|
||||
} catch (const nlohmann::json::exception& e) {
|
||||
return std::unexpected(e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// std::variant serialization/deserialization support
|
||||
// ============================================================================
|
||||
|
||||
// Helper to get the JSON type that a type serializes to
|
||||
// Used for type-based disambiguation during variant deserialization
|
||||
template<typename T>
|
||||
constexpr nlohmann::json::value_type json_type_for_type() {
|
||||
if constexpr (std::is_same_v<std::decay_t<T>, std::string>) {
|
||||
return nlohmann::json::value_t::string;
|
||||
} else if constexpr (requires { typename std::decay_t<T>::value_type; }) {
|
||||
// std::vector<T> or similar container
|
||||
return nlohmann::json::value_t::array;
|
||||
} else if constexpr (std::is_integral_v<std::decay_t<T>>) {
|
||||
return nlohmann::json::value_t::number_integer;
|
||||
} else if constexpr (std::is_floating_point_v<std::decay_t<T>>) {
|
||||
return nlohmann::json::value_t::number_float;
|
||||
} else if constexpr (std::is_same_v<std::decay_t<T>, bool>) {
|
||||
return nlohmann::json::value_t::boolean;
|
||||
} else {
|
||||
// Custom types serialize to objects
|
||||
return nlohmann::json::value_t::object;
|
||||
}
|
||||
}
|
||||
|
||||
// to_json for std::variant - serializes the active alternative
|
||||
template<typename... Alternatives>
|
||||
void to_json(nlohmann::json& j, const std::variant<Alternatives...>& v) {
|
||||
std::visit([&j](const auto& value) {
|
||||
j = value;
|
||||
}, v);
|
||||
}
|
||||
|
||||
// Helper for recursive variant alternative trying
|
||||
// Base case: no more alternatives to try
|
||||
template<typename VariantType, std::size_t CurrentIdx, std::size_t MaxIdx>
|
||||
struct variant_try_helper;
|
||||
|
||||
// Primary template: try current alternative, then recurse
|
||||
template<typename VariantType, std::size_t CurrentIdx, std::size_t MaxIdx, typename... Alternatives>
|
||||
struct variant_try_helper_impl {
|
||||
static std::expected<VariantType, std::string> apply(const nlohmann::json& j, nlohmann::json::value_type json_type) {
|
||||
using Alt = std::tuple_element_t<CurrentIdx, std::tuple<Alternatives...>>;
|
||||
|
||||
if (json_type_for_type<Alt>() == json_type) {
|
||||
auto result = json_to<Alt>(j);
|
||||
if (result.has_value()) {
|
||||
return VariantType{std::in_place_index<CurrentIdx>, std::move(result.value())};
|
||||
}
|
||||
// Deserialization failed, but JSON type matched - return error
|
||||
return std::unexpected("variant alternative " + std::to_string(CurrentIdx) + ": " + result.error());
|
||||
}
|
||||
|
||||
// JSON type doesn't match, try next alternative
|
||||
if constexpr (CurrentIdx + 1 < MaxIdx) {
|
||||
return variant_try_helper_impl<VariantType, CurrentIdx + 1, MaxIdx, Alternatives...>::apply(j, json_type);
|
||||
} else {
|
||||
return std::unexpected("no variant alternative matched JSON type");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// json_to_impl specialization for std::variant
|
||||
// Uses type-based disambiguation: matches JSON type to variant alternatives
|
||||
template<typename... Alternatives>
|
||||
struct json_to_impl<std::variant<Alternatives...>> {
|
||||
static std::expected<std::variant<Alternatives...>, std::string> apply(const nlohmann::json& j) {
|
||||
using VariantType = std::variant<Alternatives...>;
|
||||
return variant_try_helper_impl<VariantType, 0, sizeof...(Alternatives), Alternatives...>::apply(j, j.type());
|
||||
}
|
||||
};
|
||||
|
||||
// Helper macros for common registration logic used by all DRH_DEFINE_TYPE_* macros
|
||||
|
||||
// Register has_json_to<Type> trait
|
||||
@@ -122,8 +244,29 @@ std::expected<T, std::string> json_to(const nlohmann::json& j) {
|
||||
nlohmann_json_t.field = std::move(nlohmann_json_field_result.value()); \
|
||||
}
|
||||
|
||||
// Helper to serialize a value to nlohmann::json
|
||||
// Handles std::optional, std::variant, and primitive types
|
||||
template<typename T>
|
||||
nlohmann::json serialize_to_json(const T& value) {
|
||||
return value; // Works for primitives via implicit conversion
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
nlohmann::json serialize_to_json(const std::optional<T>& opt) {
|
||||
nlohmann::json j;
|
||||
::to_json(j, opt);
|
||||
return j;
|
||||
}
|
||||
|
||||
template<typename... Alternatives>
|
||||
nlohmann::json serialize_to_json(const std::variant<Alternatives...>& v) {
|
||||
nlohmann::json j;
|
||||
::to_json(j, v);
|
||||
return j;
|
||||
}
|
||||
|
||||
// to_json using nlohmann's existing pattern
|
||||
#define DRH_JSON_TO(field) nlohmann_json_j[#field] = nlohmann_json_t.field;
|
||||
#define DRH_JSON_TO(field) nlohmann_json_j[#field] = serialize_to_json(nlohmann_json_t.field);
|
||||
|
||||
// The main macro - generates to_json and json_to<Type>() template specialization
|
||||
#define DRH_DEFINE_TYPE_REQUIRED(Type, ...) \
|
||||
@@ -238,6 +381,23 @@ inline const auto json_register_vector_FunctionCall = []() {
|
||||
return 0u;
|
||||
}();
|
||||
|
||||
// Test-only variant type for validating variant serialization/deserialization
|
||||
// JSON string → std::string, JSON array → std::vector<FunctionCall>
|
||||
using TestVariantType = std::variant<std::string, std::vector<FunctionCall>>;
|
||||
|
||||
// has_json_to specialization for TestVariantType
|
||||
template<> struct has_json_to<TestVariantType> : std::true_type {};
|
||||
|
||||
// Register TestVariantType in json_dispatch_map for -R debug flag
|
||||
inline const auto drh_register_TestVariantType = []() {
|
||||
json_dispatch_map["TestVariantType"] = [](const nlohmann::json& j) {
|
||||
auto result = json_to<TestVariantType>(j);
|
||||
if (result) std::cout << "success" << std::endl;
|
||||
else std::cout << "error: " << result.error() << std::endl;
|
||||
};
|
||||
return 0u;
|
||||
}();
|
||||
|
||||
struct ToolCall {
|
||||
size_t index;
|
||||
std::string id = "";
|
||||
@@ -246,15 +406,51 @@ struct ToolCall {
|
||||
};
|
||||
DRH_DEFINE_TYPE_MIXED(ToolCall, (index), (id, type, function))
|
||||
|
||||
struct ImageUrl {
|
||||
std::string url = "";
|
||||
std::string detail = "auto";
|
||||
};
|
||||
DRH_DEFINE_TYPE_OPTIONAL(ImageUrl, url, detail)
|
||||
|
||||
struct ContentPart {
|
||||
std::string type = "text";
|
||||
std::optional<std::string> text = std::nullopt;
|
||||
std::optional<ImageUrl> image_url = std::nullopt;
|
||||
};
|
||||
|
||||
DRH_DEFINE_TYPE_OPTIONAL(ContentPart, type, text, image_url)
|
||||
|
||||
|
||||
using Content = std::variant<std::string, std::vector<ContentPart>>;
|
||||
|
||||
struct Message {
|
||||
std::string role = "";
|
||||
std::string content = "";
|
||||
Content content = "";
|
||||
std::string reasoning = "";
|
||||
std::string reasoning_content = "";
|
||||
std::string tool_call_id = "";
|
||||
std::vector<ToolCall> tool_calls = {};
|
||||
|
||||
std::string dump(int indent = -1) const;
|
||||
|
||||
// Helper to get content as string (for C++ code that needs string)
|
||||
std::string get_content_string() const {
|
||||
if (auto* str = std::get_if<std::string>(&content)) {
|
||||
return *str;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Check if content is empty (for both variant alternatives)
|
||||
bool content_empty() const {
|
||||
if (auto* str = std::get_if<std::string>(&content)) {
|
||||
return str->empty();
|
||||
}
|
||||
if (auto* vec = std::get_if<std::vector<ContentPart>>(&content)) {
|
||||
return vec->empty();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
DRH_DEFINE_TYPE_MIXED(Message, (), (role, content, reasoning, reasoning_content, tool_call_id, tool_calls))
|
||||
|
||||
@@ -262,6 +458,31 @@ inline std::string Message::dump(int indent) const {
|
||||
return nlohmann::json(*this).dump(indent);
|
||||
}
|
||||
|
||||
// Operator<< for variant content - outputs string if variant holds string,
|
||||
// or renders text parts and indicates image parts for vector<ContentPart>
|
||||
inline std::ostream& operator<<(std::ostream& os, const std::variant<std::string, std::vector<ContentPart>>& content) {
|
||||
if (auto* str = std::get_if<std::string>(&content)) {
|
||||
os << *str;
|
||||
} else if (auto* parts = std::get_if<std::vector<ContentPart>>(&content)) {
|
||||
for (const auto& part : *parts) {
|
||||
if (part.type == "text" && part.text.has_value()) {
|
||||
os << part.text.value();
|
||||
} else if (part.type == "image_url" && part.image_url.has_value()) {
|
||||
os << "[Image]";
|
||||
}
|
||||
}
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
// Append to string variant content (for streaming concatenation)
|
||||
inline void append_to_variant_content(std::variant<std::string, std::vector<ContentPart>>& content, const std::string& append) {
|
||||
if (auto* str = std::get_if<std::string>(&content)) {
|
||||
*str += append;
|
||||
}
|
||||
// If variant holds vector<ContentPart>, ignore append (multimodal content shouldn't be concatenated)
|
||||
}
|
||||
|
||||
struct Choice {
|
||||
Message delta;
|
||||
std::string finish_reason;
|
||||
@@ -412,3 +633,6 @@ DRH_DEFINE_TYPE_MIXED(MixedTypeAllOptionalTest, (), (field1, field2))
|
||||
static_assert(has_json_to_v<MixedTypeSmokeTest>, "MixedTypeSmokeTest should have json_to specialization");
|
||||
static_assert(has_json_to_v<MixedTypeAllRequiredTest>, "MixedTypeAllRequiredTest should have json_to specialization");
|
||||
static_assert(has_json_to_v<MixedTypeAllOptionalTest>, "MixedTypeAllOptionalTest should have json_to specialization");
|
||||
|
||||
// Compile-time test for variant type
|
||||
static_assert(has_json_to_v<TestVariantType>, "TestVariantType should have json_to specialization");
|
||||
|
||||
+5
-5
@@ -7,11 +7,11 @@
|
||||
|
||||
// Template function that wraps a typed lambda for tool execution
|
||||
// Handles JSON deserialization internally, eliminating std::any
|
||||
template<typename ParamsType>
|
||||
std::function<std::string(const nlohmann::json&)> create_tool_execute(
|
||||
std::function<std::string(const ParamsType&)> fn
|
||||
template<typename ParamsType, typename F>
|
||||
std::function<Content(const nlohmann::json&)> create_tool_execute(
|
||||
F const& fn
|
||||
) {
|
||||
return [fn = std::move(fn)](const nlohmann::json& json) -> std::string {
|
||||
return [fn = std::move(fn)](const nlohmann::json& json) -> Content {
|
||||
auto result = json_to<ParamsType>(json);
|
||||
if (!result) {
|
||||
return "Error: " + result.error();
|
||||
@@ -24,7 +24,7 @@ struct Tool {
|
||||
std::string name;
|
||||
std::string description;
|
||||
nlohmann::json parameters_schema;
|
||||
std::function<std::string(const nlohmann::json&)> execute;
|
||||
std::function<Content(const nlohmann::json&)> execute;
|
||||
|
||||
nlohmann::json to_api_json() const {
|
||||
return {
|
||||
|
||||
@@ -11,7 +11,7 @@ public:
|
||||
std::expected<void, std::string> register_tool(Tool tool);
|
||||
std::optional<const Tool*> find(const std::string& name) const;
|
||||
std::vector<Tool> tools() const;
|
||||
std::string invoke(const std::string& name, const nlohmann::json& args) const;
|
||||
Content invoke(const std::string& name, const nlohmann::json& args) const;
|
||||
std::expected<ToolRegistry, std::string> create_subregistry(std::vector<std::string> const& names) const;
|
||||
std::vector<std::string> const& tool_names() const;
|
||||
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
#include "base64_util.hpp"
|
||||
#include <unordered_map>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
||||
static const std::string base64_chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
static inline bool is_base64(unsigned char c) {
|
||||
return (isalnum(c) || (c == '+') || (c == '/'));
|
||||
}
|
||||
|
||||
std::string base64_encode(const std::string& input) {
|
||||
std::string ret;
|
||||
int i = 0;
|
||||
unsigned char char_array_3[3];
|
||||
unsigned char char_array_4[4];
|
||||
const std::string& chars = input;
|
||||
int in_len = static_cast<int>(chars.size());
|
||||
int pos = 0;
|
||||
|
||||
while (in_len--) {
|
||||
char_array_3[i++] = chars[pos++];
|
||||
if (i == 3) {
|
||||
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
|
||||
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
|
||||
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
|
||||
char_array_4[3] = char_array_3[2] & 0x3f;
|
||||
|
||||
for (i = 0; (i < 4); i++)
|
||||
ret += base64_chars[char_array_4[i]];
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (i) {
|
||||
for (int j = i; j < 3; j++)
|
||||
char_array_3[j] = '\0';
|
||||
|
||||
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
|
||||
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
|
||||
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
|
||||
char_array_4[3] = char_array_3[2] & 0x3f;
|
||||
|
||||
for (int j = 0; (j < i + 1); j++)
|
||||
ret += base64_chars[char_array_4[j]];
|
||||
|
||||
while ((i++ < 3))
|
||||
ret += '=';
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::expected<std::string, std::string> base64_decode(const std::string& input) {
|
||||
int in_len = static_cast<int>(input.size());
|
||||
int i = 0;
|
||||
int in_pos = 0;
|
||||
unsigned char char_array_4[4], char_array_3[3];
|
||||
std::string ret;
|
||||
|
||||
while (in_len-- && (input[in_pos] != '=') && is_base64(input[in_pos])) {
|
||||
char_array_4[i++] = input[in_pos]; in_pos++;
|
||||
if (i == 4) {
|
||||
for (i = 0; i < 4; i++)
|
||||
char_array_4[i] = static_cast<unsigned char>(base64_chars.find(char_array_4[i]));
|
||||
|
||||
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
|
||||
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
|
||||
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
|
||||
|
||||
for (i = 0; (i < 3); i++)
|
||||
ret += static_cast<char>(char_array_3[i]);
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (i) {
|
||||
for (int j = i; j < 4; j++)
|
||||
char_array_4[j] = 0;
|
||||
|
||||
for (int j = 0; j < 4; j++)
|
||||
char_array_4[j] = static_cast<unsigned char>(base64_chars.find(char_array_4[j]));
|
||||
|
||||
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
|
||||
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
|
||||
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
|
||||
|
||||
for (int j = 0; (j < i - 1); j++)
|
||||
ret += static_cast<char>(char_array_3[j]);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string get_mime_type_from_extension(const std::string& extension) {
|
||||
static const std::unordered_map<std::string, std::string> mime_map = {
|
||||
{"png", "png"},
|
||||
{"jpg", "jpeg"},
|
||||
{"jpeg", "jpeg"},
|
||||
{"gif", "gif"},
|
||||
{"webp", "webp"},
|
||||
{"bmp", "bmp"},
|
||||
{"tiff", "tiff"},
|
||||
{"tif", "tiff"},
|
||||
{"svg", "svg+xml"},
|
||||
};
|
||||
|
||||
// Convert extension to lowercase
|
||||
std::string lower_ext = extension;
|
||||
for (auto& c : lower_ext) {
|
||||
c = static_cast<char>(tolower(c));
|
||||
}
|
||||
|
||||
// Remove leading dot if present
|
||||
if (!lower_ext.empty() && lower_ext[0] == '.') {
|
||||
lower_ext = lower_ext.substr(1);
|
||||
}
|
||||
|
||||
auto it = mime_map.find(lower_ext);
|
||||
if (it != mime_map.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Default to png if unknown
|
||||
return "png";
|
||||
}
|
||||
|
||||
std::string generate_data_uri(const std::string& extension, const std::string& base64_data) {
|
||||
std::string mime_type = get_mime_type_from_extension(extension);
|
||||
return "data:image/" + mime_type + ";base64," + base64_data;
|
||||
}
|
||||
|
||||
std::expected<std::string, std::string> read_image_as_data_uri(const std::string& file_path) {
|
||||
// Check if file exists
|
||||
if (!std::filesystem::exists(file_path)) {
|
||||
return std::unexpected("Image file not found: " + file_path);
|
||||
}
|
||||
|
||||
// Check if it's a regular file
|
||||
if (!std::filesystem::is_regular_file(file_path)) {
|
||||
return std::unexpected("Not a regular file: " + file_path);
|
||||
}
|
||||
|
||||
// Open and read the file
|
||||
std::ifstream file(file_path, std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
return std::unexpected("Cannot open image file: " + file_path);
|
||||
}
|
||||
|
||||
// Read file contents
|
||||
std::string content((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
file.close();
|
||||
|
||||
// Get file extension
|
||||
std::filesystem::path p(file_path);
|
||||
std::string extension = p.extension().string();
|
||||
|
||||
// Base64 encode the content
|
||||
std::string encoded = base64_encode(content);
|
||||
|
||||
// Generate data URI
|
||||
return generate_data_uri(extension, encoded);
|
||||
}
|
||||
+48
-8
@@ -1,5 +1,6 @@
|
||||
#include "harness.hpp"
|
||||
#include "json_types.hpp"
|
||||
#include "base64_util.hpp"
|
||||
#include <iostream>
|
||||
|
||||
Harness::Harness(std::unique_ptr<Client> client, const ToolRegistry& registry,
|
||||
@@ -43,8 +44,40 @@ std::expected<std::string, std::string> Harness::chat(const std::string& user_me
|
||||
Message message{};
|
||||
message.role = "user";
|
||||
message.content = user_message;
|
||||
return chat(message);
|
||||
}
|
||||
|
||||
push_message(message);
|
||||
std::expected<std::string, std::string> Harness::chat(const std::string& user_message, const std::vector<std::string>& image_paths) {
|
||||
Message message{};
|
||||
message.role = "user";
|
||||
std::vector<ContentPart> content_parts;
|
||||
|
||||
// Add text content part
|
||||
ContentPart text_part;
|
||||
text_part.type = "text";
|
||||
text_part.text = user_message;
|
||||
content_parts.push_back(text_part);
|
||||
|
||||
// Read image files and add image content parts
|
||||
for (const auto& path : image_paths) {
|
||||
auto result = read_image_as_data_uri(path);
|
||||
if (!result) {
|
||||
return std::unexpected("failed to read image '" + path + "': " + result.error());
|
||||
}
|
||||
ContentPart image_part;
|
||||
image_part.type = "image_url";
|
||||
ImageUrl img_url;
|
||||
img_url.url = result.value();
|
||||
image_part.image_url = img_url;
|
||||
content_parts.push_back(image_part);
|
||||
}
|
||||
|
||||
message.content = content_parts;
|
||||
return chat(message);
|
||||
}
|
||||
|
||||
std::expected<std::string, std::string> Harness::chat(const Message& user_message) {
|
||||
push_message(user_message);
|
||||
|
||||
for (size_t i = 0; i < config_.max_tool_iterations; ++i) {
|
||||
Message response;
|
||||
@@ -68,7 +101,7 @@ std::expected<std::string, std::string> Harness::chat(const std::string& user_me
|
||||
|
||||
push_message(response);
|
||||
|
||||
if (response.content.empty() && response.tool_calls.empty()) {
|
||||
if (response.content_empty() && response.tool_calls.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -81,7 +114,7 @@ std::expected<std::string, std::string> Harness::chat(const std::string& user_me
|
||||
if (config_.verbose)
|
||||
std::cerr << prefix << "[tool call] " << tool_name << "(" << args_str << ")\n";
|
||||
|
||||
std::string result;
|
||||
Content result = "";
|
||||
auto args_json_result = parse_json_safely(args_str);
|
||||
if (!args_json_result) {
|
||||
result = "Error: invalid JSON in tool arguments";
|
||||
@@ -90,9 +123,16 @@ std::expected<std::string, std::string> Harness::chat(const std::string& user_me
|
||||
}
|
||||
|
||||
if (config_.verbose)
|
||||
std::cerr << prefix << "[tool result] "
|
||||
<< result.substr(0, 300)
|
||||
<< (result.size() > 300 ? "..." : "") << "\n";
|
||||
{
|
||||
std::cerr << prefix << "[tool result] ";
|
||||
if (std::holds_alternative<std::string>(result))
|
||||
{
|
||||
std::string str = std::get<std::string>(result);
|
||||
std::cerr
|
||||
<< str.substr(0, 300)
|
||||
<< (str.size() > 300 ? "..." : "") << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
Message tool_result{};
|
||||
tool_result.tool_call_id = tc.id;
|
||||
@@ -103,8 +143,8 @@ std::expected<std::string, std::string> Harness::chat(const std::string& user_me
|
||||
continue;
|
||||
}
|
||||
|
||||
// No tool calls — return content
|
||||
return response.content;
|
||||
// No tool calls — return content as string
|
||||
return response.get_content_string();
|
||||
}
|
||||
|
||||
return std::unexpected("max tool iterations exceeded");
|
||||
|
||||
+98
-9
@@ -7,11 +7,13 @@
|
||||
#include "agent_profile.hpp"
|
||||
#include "rebase_engine.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "base64_util.hpp"
|
||||
#if !defined(_WIN32)
|
||||
#include "shell_tool.hpp"
|
||||
#endif
|
||||
#include "agent_tool.hpp"
|
||||
#include "edit_tool.hpp"
|
||||
#include "read_multi_modal.hpp"
|
||||
#include "file_list_tool.hpp"
|
||||
#include "file_read_tool.hpp"
|
||||
#include "grep_tool.hpp"
|
||||
@@ -109,7 +111,7 @@ 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_history_size = nullptr;
|
||||
char const* test_rebase_content = nullptr;
|
||||
char const* test_build_rebase_json = nullptr;
|
||||
char const* test_apply_rebase_lines = nullptr;
|
||||
@@ -117,10 +119,17 @@ int main(int argc, char** argv) {
|
||||
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;
|
||||
char const* test_data_uri_ext = nullptr;
|
||||
char const* test_data_uri_b64 = nullptr;
|
||||
char const* test_image_file = nullptr;
|
||||
|
||||
ToolRegistry master_registery;
|
||||
ToolRegistry agent_tools;
|
||||
expect_or_exit(master_registery.register_tool(make_edit_tool()));
|
||||
expect_or_exit(master_registery.register_tool(make_read_multi_modal()));
|
||||
expect_or_exit(master_registery.register_tool(make_file_list_tool()));
|
||||
expect_or_exit(master_registery.register_tool(make_file_read_tool()));
|
||||
expect_or_exit(master_registery.register_tool(make_grep_tool()));
|
||||
@@ -155,6 +164,7 @@ int main(int argc, char** argv) {
|
||||
bool full_history = false;
|
||||
bool edit_history = false;
|
||||
std::vector<std::string> skill_filters;
|
||||
std::vector<std::string> image_paths;
|
||||
|
||||
consume_arg(); // progname
|
||||
|
||||
@@ -231,13 +241,13 @@ int main(int argc, char** argv) {
|
||||
test_type_required_type = consume_arg();
|
||||
test_type_required_json = consume_arg();
|
||||
}
|
||||
else if (argv[0] == std::string("-P"))
|
||||
else if (argv[0] == std::string("-P"))
|
||||
{
|
||||
consume_arg();
|
||||
test_rebase_history_size = consume_arg();
|
||||
test_rebase_content = consume_arg();
|
||||
}
|
||||
else if (argv[0] == std::string("-B"))
|
||||
else if (argv[0] == std::string("-Z"))
|
||||
{
|
||||
consume_arg();
|
||||
test_build_rebase_json = consume_arg();
|
||||
@@ -248,13 +258,34 @@ int main(int argc, char** argv) {
|
||||
test_apply_rebase_lines = consume_arg();
|
||||
test_apply_rebase_history = consume_arg();
|
||||
}
|
||||
else if (argv[0] == std::string("-T"))
|
||||
else if (argv[0] == std::string("-B"))
|
||||
{
|
||||
consume_arg();
|
||||
test_base64_encode = consume_arg();
|
||||
}
|
||||
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"))
|
||||
{
|
||||
consume_arg();
|
||||
test_mime_type = consume_arg();
|
||||
}
|
||||
else if (argv[0] == std::string("-U"))
|
||||
{
|
||||
consume_arg();
|
||||
test_data_uri_ext = consume_arg();
|
||||
test_data_uri_b64 = consume_arg();
|
||||
}
|
||||
else if (argv[0] == std::string("-L"))
|
||||
{
|
||||
consume_arg();
|
||||
@@ -278,11 +309,21 @@ 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"))
|
||||
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();
|
||||
image_paths.push_back(consume_arg());
|
||||
}
|
||||
else if (argv[0] == std::string("-I"))
|
||||
{
|
||||
consume_arg();
|
||||
test_image_file = consume_arg();
|
||||
}
|
||||
else if (argv[0] == std::string("-h"))
|
||||
{
|
||||
std::cout << "Usage: drharness [options]\n"
|
||||
@@ -291,6 +332,7 @@ int main(int argc, char** argv) {
|
||||
<< " -p <file> Pretty print a saved conversation JSON file\n"
|
||||
<< " -t <tool> Tool name (can be specified multiple times)\n"
|
||||
<< " -s <name> Skill name to enable (can be specified multiple times)\n"
|
||||
<< " -i <path> Image file to attach (can be specified multiple times)\n"
|
||||
<< " -n Start a new conversation (auto-generates temp file if -F not specified)\n"
|
||||
<< " -m <prompt> Prompt message (required)\n"
|
||||
<< " -M <model> Ollama model name\n"
|
||||
@@ -306,10 +348,16 @@ 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"
|
||||
<< " -R <type> <json> Debug: test json_to<Type>() for required fields and exit\n"
|
||||
<< " -P <size> <content> Debug: test rebase parser and exit\n"
|
||||
<< " -B <json> Debug: test build_rebase_content with JSON history 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"
|
||||
<< " -T <json> <index> <role> Debug: test build_edit_prompt and exit\n"
|
||||
<< " -W <json> <index> <role> Debug: test build_edit_prompt and exit\n"
|
||||
<< " -B <string> Debug: test base64 encode 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"
|
||||
<< " -d Use debug client (mock API via stdin)\n"
|
||||
<< " -h Display this help menu\n";
|
||||
exit(0);
|
||||
@@ -476,7 +524,7 @@ int main(int argc, char** argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (test_edit_prompt_json && test_edit_prompt_index && test_edit_prompt_role) {
|
||||
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));
|
||||
@@ -487,6 +535,41 @@ int main(int argc, char** argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (test_base64_encode) {
|
||||
std::cout << base64_encode(test_base64_encode) << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (test_base64_decode) {
|
||||
auto result = base64_decode(test_base64_decode);
|
||||
if (!result) {
|
||||
std::cout << "error: " << result.error() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
std::cout << result.value() << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (test_mime_type) {
|
||||
std::cout << get_mime_type_from_extension(test_mime_type) << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (test_data_uri_ext && test_data_uri_b64) {
|
||||
std::cout << generate_data_uri(test_data_uri_ext, test_data_uri_b64) << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (test_image_file) {
|
||||
auto result = read_image_as_data_uri(test_image_file);
|
||||
if (!result) {
|
||||
std::cerr << "Error: " << result.error() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
std::cout << result.value() << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (list_models)
|
||||
{
|
||||
auto models_result = create_client(use_debug)->list_models();
|
||||
@@ -580,7 +663,13 @@ int main(int argc, char** argv) {
|
||||
std::cerr << "Prompt input:" << std::endl;
|
||||
std::cerr << final_prompt << std::endl;
|
||||
|
||||
auto reply = harness.chat(final_prompt);
|
||||
std::expected<std::string, std::string> reply;
|
||||
|
||||
if (!image_paths.empty()) {
|
||||
reply = harness.chat(final_prompt, image_paths);
|
||||
} else {
|
||||
reply = harness.chat(final_prompt);
|
||||
}
|
||||
if (!reply) {
|
||||
std::cerr << "Error: " << reply.error() << std::endl;
|
||||
return 1;
|
||||
|
||||
@@ -160,13 +160,13 @@ std::expected<Message, std::string> OllamaClient::chat(const std::vector<Message
|
||||
|
||||
//std::cerr << new_packet << std::endl;
|
||||
|
||||
total.content += partial_message->content;
|
||||
append_to_variant_content(total.content, partial_message->get_content_string());
|
||||
total.reasoning_content += partial_message->reasoning_content;
|
||||
total.reasoning += partial_message->reasoning;
|
||||
total.role += partial_message->role;
|
||||
total.tool_call_id += partial_message->tool_call_id;
|
||||
|
||||
std::string to_write = partial_message->content
|
||||
std::string to_write = partial_message->get_content_string()
|
||||
+ partial_message->reasoning_content
|
||||
+ partial_message->reasoning;
|
||||
|
||||
@@ -283,7 +283,7 @@ void print_history(std::ostream& out, const nlohmann::json& data, bool colored)
|
||||
}
|
||||
}
|
||||
|
||||
if (!msg.content.empty()) {
|
||||
if (!msg.content_empty()) {
|
||||
if (colored) out << "\033[37m[Ai Response]\033[0m ";
|
||||
else out << "[Ai Response] ";
|
||||
out << msg.content << "\n\n";
|
||||
|
||||
@@ -123,7 +123,7 @@ std::string HistoryEditor::get_message_display(const Message& msg) const {
|
||||
}
|
||||
|
||||
// Get content from message
|
||||
std::string content = msg.content;
|
||||
std::string content = msg.get_content_string();
|
||||
|
||||
// If no content, show (empty)
|
||||
if (content.empty()) {
|
||||
|
||||
@@ -24,8 +24,8 @@ std::vector<Tool> ToolRegistry::tools() const {
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string ToolRegistry::invoke(const std::string& name,
|
||||
const nlohmann::json& args) const {
|
||||
Content ToolRegistry::invoke(const std::string& name,
|
||||
const nlohmann::json& args) const {
|
||||
auto it = tools_.find(name);
|
||||
if (it == tools_.end())
|
||||
return "Error: unknown tool: " + name;
|
||||
|
||||
@@ -19,6 +19,7 @@ add_dr_test(test_tool_read_file)
|
||||
add_dr_test(test_tool_list_files)
|
||||
add_dr_test(test_tool_grep)
|
||||
add_dr_test(test_tool_shell)
|
||||
add_dr_test(test_tool_read_multi_modal)
|
||||
|
||||
# Add feature validation tests
|
||||
add_dr_test(test_tool_restrictions)
|
||||
@@ -48,6 +49,9 @@ add_dr_test(test_json_type_mixed)
|
||||
# Add json_to_field tests
|
||||
add_dr_test(test_json_to_field)
|
||||
|
||||
# Add std::variant JSON serialization tests
|
||||
add_dr_test(test_json_variant)
|
||||
|
||||
# Add expected error handling tests
|
||||
add_dr_test(test_expected_errors)
|
||||
|
||||
@@ -134,3 +138,21 @@ 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)
|
||||
|
||||
# Add image file reading tests
|
||||
add_dr_test(test_image_flag)
|
||||
|
||||
# Add ImageUrl and ContentPart tests
|
||||
add_dr_test(test_image_url)
|
||||
|
||||
# Add multimodal message construction tests
|
||||
add_dr_test(test_multimodal_message)
|
||||
|
||||
# Add multimodal round-trip persistence tests
|
||||
add_dr_test(test_multimodal_roundtrip)
|
||||
|
||||
# Add print_history multimodal tests
|
||||
add_dr_test(test_print_history_multimodal)
|
||||
|
||||
@@ -387,6 +387,74 @@ function create_state() {
|
||||
}
|
||||
export -f create_state
|
||||
|
||||
# Get message role from chat history at given index
|
||||
function dr_history_role() {
|
||||
local chat_file="$1"
|
||||
local index="$2"
|
||||
jq -r --argjson i "$index" '.history[$i].role' "$chat_file"
|
||||
}
|
||||
export -f dr_history_role
|
||||
|
||||
# Get content type (string/array) from message at given history index
|
||||
function dr_content_type() {
|
||||
local chat_file="$1"
|
||||
local index="$2"
|
||||
jq -r --argjson i "$index" '.history[$i].content | type' "$chat_file"
|
||||
}
|
||||
export -f dr_content_type
|
||||
|
||||
# Get content part type at given history and content part indices
|
||||
function dr_content_part_type() {
|
||||
local chat_file="$1"
|
||||
local history_idx="$2"
|
||||
local part_idx="$3"
|
||||
jq -r --argjson h "$history_idx" --argjson p "$part_idx" '.history[$h].content[$p].type' "$chat_file"
|
||||
}
|
||||
export -f dr_content_part_type
|
||||
|
||||
# Get text content from a content part at given history and content part indices
|
||||
function dr_content_part_text() {
|
||||
local chat_file="$1"
|
||||
local history_idx="$2"
|
||||
local part_idx="$3"
|
||||
jq -r --argjson h "$history_idx" --argjson p "$part_idx" '.history[$h].content[$p].text' "$chat_file"
|
||||
}
|
||||
export -f dr_content_part_text
|
||||
|
||||
# Get image URL from a content part at given history and content part indices
|
||||
function dr_content_part_image_url() {
|
||||
local chat_file="$1"
|
||||
local history_idx="$2"
|
||||
local part_idx="$3"
|
||||
jq -r --argjson h "$history_idx" --argjson p "$part_idx" '.history[$h].content[$p].image_url.url' "$chat_file"
|
||||
}
|
||||
export -f dr_content_part_image_url
|
||||
|
||||
# Get image detail from a content part at given history and content part indices
|
||||
function dr_content_part_image_detail() {
|
||||
local chat_file="$1"
|
||||
local history_idx="$2"
|
||||
local part_idx="$3"
|
||||
jq -r --argjson h "$history_idx" --argjson p "$part_idx" '.history[$h].content[$p].image_url.detail' "$chat_file"
|
||||
}
|
||||
export -f dr_content_part_image_detail
|
||||
|
||||
# Get content parts count from message at given history index
|
||||
function dr_content_parts_count() {
|
||||
local chat_file="$1"
|
||||
local index="$2"
|
||||
jq --argjson i "$index" '.history[$i].content | length' "$chat_file"
|
||||
}
|
||||
export -f dr_content_parts_count
|
||||
|
||||
# Get content from message at given history index (for text-only messages)
|
||||
function dr_content_text() {
|
||||
local chat_file="$1"
|
||||
local index="$2"
|
||||
jq -r --argjson i "$index" '.history[$i].content' "$chat_file"
|
||||
}
|
||||
export -f dr_content_text
|
||||
|
||||
# Resolve paths relative to this script
|
||||
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
PROJECT_ROOT=$(cd "${SCRIPT_DIR}/.." && pwd)
|
||||
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/bin/bash
|
||||
# test_base64_encode.sh - Tests for base64 encoding utility
|
||||
|
||||
# Test 1: Basic base64 encoding
|
||||
OUTPUT=$("${DRHARNESS}" -B "Hello, World!")
|
||||
if [ "${OUTPUT}" != "SGVsbG8sIFdvcmxkIQ==" ]; then
|
||||
echo "FAIL: base64_encode - expected 'SGVsbG8sIFdvcmxkIQ==', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 2: Empty string encoding
|
||||
OUTPUT=$("${DRHARNESS}" -B "")
|
||||
if [ "${OUTPUT}" != "" ]; then
|
||||
echo "FAIL: base64_encode empty - expected empty string, got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 3: Base64 decoding
|
||||
OUTPUT=$("${DRHARNESS}" -K "SGVsbG8sIFdvcmxkIQ==")
|
||||
if [ "${OUTPUT}" != "Hello, World!" ]; then
|
||||
echo "FAIL: base64_decode - expected 'Hello, World!', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 4: Round-trip encoding/decoding
|
||||
ORIGINAL="Test string with special chars: !@#$%^&*()"
|
||||
ENCODED=$("${DRHARNESS}" -B "${ORIGINAL}")
|
||||
DECODED=$("${DRHARNESS}" -K "${ENCODED}")
|
||||
if [ "${DECODED}" != "${ORIGINAL}" ]; then
|
||||
echo "FAIL: round-trip - expected '${ORIGINAL}', got '${DECODED}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 5: MIME type detection - PNG
|
||||
OUTPUT=$("${DRHARNESS}" -T "png")
|
||||
if [ "${OUTPUT}" != "png" ]; then
|
||||
echo "FAIL: mime_type png - expected 'png', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 6: MIME type detection - JPG
|
||||
OUTPUT=$("${DRHARNESS}" -T "jpg")
|
||||
if [ "${OUTPUT}" != "jpeg" ]; then
|
||||
echo "FAIL: mime_type jpg - expected 'jpeg', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 7: MIME type detection - JPEG
|
||||
OUTPUT=$("${DRHARNESS}" -T "jpeg")
|
||||
if [ "${OUTPUT}" != "jpeg" ]; then
|
||||
echo "FAIL: mime_type jpeg - expected 'jpeg', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 8: MIME type detection - GIF
|
||||
OUTPUT=$("${DRHARNESS}" -T "gif")
|
||||
if [ "${OUTPUT}" != "gif" ]; then
|
||||
echo "FAIL: mime_type gif - expected 'gif', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 9: MIME type detection - WEBP
|
||||
OUTPUT=$("${DRHARNESS}" -T "webp")
|
||||
if [ "${OUTPUT}" != "webp" ]; then
|
||||
echo "FAIL: mime_type webp - expected 'webp', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 10: MIME type detection - case insensitive (uppercase)
|
||||
OUTPUT=$("${DRHARNESS}" -T "PNG")
|
||||
if [ "${OUTPUT}" != "png" ]; then
|
||||
echo "FAIL: mime_type PNG - expected 'png', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 11: MIME type detection - with leading dot
|
||||
OUTPUT=$("${DRHARNESS}" -T ".jpg")
|
||||
if [ "${OUTPUT}" != "jpeg" ]; then
|
||||
echo "FAIL: mime_type .jpg - expected 'jpeg', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 12: MIME type detection - unknown extension defaults to png
|
||||
OUTPUT=$("${DRHARNESS}" -T "xyz")
|
||||
if [ "${OUTPUT}" != "png" ]; then
|
||||
echo "FAIL: mime_type unknown - expected 'png', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 13: Data URI generation - PNG
|
||||
OUTPUT=$("${DRHARNESS}" -U "png" "SGVsbG8=")
|
||||
if [ "${OUTPUT}" != "data:image/png;base64,SGVsbG8=" ]; then
|
||||
echo "FAIL: data_uri png - expected 'data:image/png;base64,SGVsbG8=', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 14: Data URI generation - JPG
|
||||
OUTPUT=$("${DRHARNESS}" -U "jpg" "SGVsbG8=")
|
||||
if [ "${OUTPUT}" != "data:image/jpeg;base64,SGVsbG8=" ]; then
|
||||
echo "FAIL: data_uri jpg - expected 'data:image/jpeg;base64,SGVsbG8=', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 15: Data URI generation - GIF
|
||||
OUTPUT=$("${DRHARNESS}" -U "gif" "SGVsbG8=")
|
||||
if [ "${OUTPUT}" != "data:image/gif;base64,SGVsbG8=" ]; then
|
||||
echo "FAIL: data_uri gif - expected 'data:image/gif;base64,SGVsbG8=', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All base64 encode tests passed"
|
||||
@@ -5,7 +5,7 @@ test_name="edit_first_message"
|
||||
|
||||
json=$(create_history_json user "hello" assistant "hi there")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -T "$json" "0" "user")
|
||||
OUTPUT=$("${DRHARNESS}" -W "$json" "0" "user")
|
||||
|
||||
# Check for "no previous message" text
|
||||
if ! echo "$OUTPUT" | grep -q "# (no previous message)"; then
|
||||
|
||||
@@ -5,7 +5,7 @@ test_name="edit_middle_message"
|
||||
|
||||
json=$(create_history_json user "msg0" assistant "msg1" tool "msg2")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -T "$json" "2" "tool")
|
||||
OUTPUT=$("${DRHARNESS}" -W "$json" "2" "tool")
|
||||
|
||||
# Should show message 1 as context (immediate previous)
|
||||
if ! echo "$OUTPUT" | grep -q "\[1\] assistant: msg1"; then
|
||||
|
||||
@@ -5,7 +5,7 @@ test_name="multiline_context"
|
||||
|
||||
json=$(create_history_json user $'line one\nline two' assistant "response")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -T "$json" "1" "assistant")
|
||||
OUTPUT=$("${DRHARNESS}" -W "$json" "1" "assistant")
|
||||
|
||||
# Both lines should appear in context
|
||||
if ! echo "$OUTPUT" | grep -q "line one"; then
|
||||
|
||||
@@ -5,7 +5,7 @@ test_name="separator_placement"
|
||||
|
||||
json=$(create_history_json user "first" assistant "second")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -T "$json" "1" "assistant")
|
||||
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
|
||||
|
||||
@@ -5,7 +5,7 @@ test_name="edit_tool_role"
|
||||
|
||||
json=$(create_history_json assistant "call tool" tool "result")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -T "$json" "1" "tool")
|
||||
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
|
||||
|
||||
@@ -6,7 +6,7 @@ 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}" -T "$json" "1" "assistant")
|
||||
OUTPUT=$("${DRHARNESS}" -W "$json" "1" "assistant")
|
||||
|
||||
# Check for previous message context
|
||||
if ! echo "$OUTPUT" | grep -q "# Previous message:"; then
|
||||
|
||||
@@ -51,8 +51,9 @@ test_max_tool_iterations() {
|
||||
|
||||
# Generate 1001 tool call responses (exceeds default max of 1000)
|
||||
> mock_responses.txt
|
||||
resp_line=$(tool_call read_file '{"path":"test_file.txt"}')
|
||||
for i in $(seq 1 1001); do
|
||||
tool_call read_file '{"path":"test_file.txt"}' >> mock_responses.txt
|
||||
echo "$resp_line" >> mock_responses.txt
|
||||
done
|
||||
be_done "final response" >> mock_responses.txt
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
# test_image_flag.sh - Tests for -i CLI flag with image file reading
|
||||
|
||||
# Test 1: Nonexistent file produces clear error and non-zero exit
|
||||
OUTPUT=$("${DRHARNESS}" -I "nonexistent.png" 2>&1) || true
|
||||
if ! echo "${OUTPUT}" | grep -q "Image file not found"; then
|
||||
echo "FAIL: nonexistent file error - expected 'Image file not found', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 2: Create a test image file and read it
|
||||
# Create a minimal 1x1 PNG file
|
||||
printf '\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53\xDE\x00\x00\x00\x0C\x49\x44\x41\x54\x08\xD7\x63\xF8\xCF\xC0\x00\x00\x03\x01\x01\x00\x18\xDD\x8D\xB4\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82' > "${TEST_TEMP_DIR}/test.png"
|
||||
|
||||
# Test 3: Valid PNG file is read and returns data URI
|
||||
OUTPUT=$("${DRHARNESS}" -I "${TEST_TEMP_DIR}/test.png" 2>&1)
|
||||
if ! echo "${OUTPUT}" | grep -q "^data:image/png;base64,"; then
|
||||
echo "FAIL: valid PNG file - expected data URI starting with 'data:image/png;base64,', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 4: Create a minimal JPEG file
|
||||
printf '\xFF\xD8\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xFF\xD9' > "${TEST_TEMP_DIR}/test.jpg"
|
||||
|
||||
# Test 5: Valid JPG file is read and returns correct data URI
|
||||
OUTPUT=$("${DRHARNESS}" -I "${TEST_TEMP_DIR}/test.jpg" 2>&1)
|
||||
if ! echo "${OUTPUT}" | grep -q "^data:image/jpeg;base64,"; then
|
||||
echo "FAIL: valid JPG file - expected data URI starting with 'data:image/jpeg;base64,', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 6: Create a GIF file
|
||||
printf '\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\xFF\xFF\xFF\x00\x00\x00\x21\xF9\x04\x01\x00\x00\x00\x00\x2C\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00\x3B' > "${TEST_TEMP_DIR}/test.gif"
|
||||
|
||||
# Test 7: Valid GIF file is read and returns correct data URI
|
||||
OUTPUT=$("${DRHARNESS}" -I "${TEST_TEMP_DIR}/test.gif" 2>&1)
|
||||
if ! echo "${OUTPUT}" | grep -q "^data:image/gif;base64,"; then
|
||||
echo "FAIL: valid GIF file - expected data URI starting with 'data:image/gif;base64,', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 8: Not a file (directory) produces error
|
||||
mkdir -p "${TEST_TEMP_DIR}/not_a_file"
|
||||
OUTPUT=$("${DRHARNESS}" -I "${TEST_TEMP_DIR}/not_a_file" 2>&1) || true
|
||||
if ! echo "${OUTPUT}" | grep -q "Not a regular file"; then
|
||||
echo "FAIL: directory error - expected 'Not a regular file', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 9: Multiple images - second file nonexistent should produce error
|
||||
printf '\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53\xDE\x00\x00\x00\x0C\x49\x44\x41\x54\x08\xD7\x63\xF8\xCF\xC0\x00\x00\x03\x01\x01\x00\x18\xDD\x8D\xB4\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82' > "${TEST_TEMP_DIR}/valid.png"
|
||||
OUTPUT=$("${DRHARNESS}" -i "${TEST_TEMP_DIR}/valid.png" -i "nonexistent.png" -F "${TEST_TEMP_DIR}/chat.json" -n -m "test" 2>&1) || true
|
||||
if ! echo "${OUTPUT}" | grep -q "Image file not found"; then
|
||||
echo "FAIL: multiple images with nonexistent second file - expected error, got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All image flag tests passed"
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
# test_image_url.sh - Tests for ImageUrl and ContentPart with image_url support
|
||||
|
||||
# Test ImageUrl deserialization with url and detail fields
|
||||
OUTPUT=$("${DRHARNESS}" -R ImageUrl '{"url": "data:image/png;base64,abc123", "detail": "auto"}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ImageUrl with url and detail - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test ImageUrl deserialization with only url field (detail should default to "auto")
|
||||
OUTPUT=$("${DRHARNESS}" -R ImageUrl '{"url": "https://example.com/image.png"}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ImageUrl with only url - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test ImageUrl deserialization with empty object (should use defaults)
|
||||
OUTPUT=$("${DRHARNESS}" -R ImageUrl '{}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ImageUrl empty object - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test ContentPart with text type
|
||||
OUTPUT=$("${DRHARNESS}" -R ContentPart '{"type": "text", "text": "Hello world"}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ContentPart with text type - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test ContentPart with text type and only type field (text should be optional)
|
||||
OUTPUT=$("${DRHARNESS}" -R ContentPart '{"type": "text"}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ContentPart with only type text - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test ContentPart with image_url type
|
||||
OUTPUT=$("${DRHARNESS}" -R ContentPart '{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc123", "detail": "auto"}}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ContentPart with image_url type - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test ContentPart with image_url type and minimal image_url (only url)
|
||||
OUTPUT=$("${DRHARNESS}" -R ContentPart '{"type": "image_url", "image_url": {"url": "https://example.com/image.png"}}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ContentPart with minimal image_url - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test ContentPart with empty object (should use defaults)
|
||||
OUTPUT=$("${DRHARNESS}" -R ContentPart '{}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ContentPart empty object - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test ContentPart with image_url type and detail "low"
|
||||
OUTPUT=$("${DRHARNESS}" -R ContentPart '{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc", "detail": "low"}}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ContentPart with image_url detail low - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test ContentPart with image_url type and detail "high"
|
||||
OUTPUT=$("${DRHARNESS}" -R ContentPart '{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc", "detail": "high"}}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ContentPart with image_url detail high - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All ImageUrl and ContentPart tests passed!"
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
# test_json_variant.sh - Tests for std::variant JSON serialization/deserialization
|
||||
|
||||
# Test TestVariantType with string alternative (JSON string)
|
||||
OUTPUT=$("${DRHARNESS}" -R TestVariantType '"hello world"')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: TestVariantType string alternative - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test TestVariantType with vector alternative (JSON array with valid FunctionCall)
|
||||
OUTPUT=$("${DRHARNESS}" -R TestVariantType '[{"name": "test", "arguments": "{}"}]')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: TestVariantType vector alternative - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test TestVariantType with empty vector
|
||||
OUTPUT=$("${DRHARNESS}" -R TestVariantType '[]')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: TestVariantType empty vector - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test TestVariantType with multiple FunctionCalls in vector
|
||||
OUTPUT=$("${DRHARNESS}" -R TestVariantType '[{"name": "func1", "arguments": "{}"}, {"name": "func2", "arguments": "{}"}]')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: TestVariantType multiple items in vector - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test TestVariantType with invalid JSON type (number - no matching alternative)
|
||||
OUTPUT=$("${DRHARNESS}" -R TestVariantType '123')
|
||||
if [ "${OUTPUT}" != "error: no variant alternative matched JSON type" ]; then
|
||||
echo "FAIL: TestVariantType number type - expected error, got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test TestVariantType with invalid JSON type (object - no matching alternative)
|
||||
OUTPUT=$("${DRHARNESS}" -R TestVariantType '{"name": "test"}')
|
||||
if [ "${OUTPUT}" != "error: no variant alternative matched JSON type" ]; then
|
||||
echo "FAIL: TestVariantType object type - expected error, got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test TestVariantType with invalid JSON type (boolean - no matching alternative)
|
||||
OUTPUT=$("${DRHARNESS}" -R TestVariantType 'true')
|
||||
if [ "${OUTPUT}" != "error: no variant alternative matched JSON type" ]; then
|
||||
echo "FAIL: TestVariantType boolean type - expected error, got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test TestVariantType with empty string (valid string alternative)
|
||||
OUTPUT=$("${DRHARNESS}" -R TestVariantType '""')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: TestVariantType empty string - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test TestVariantType with vector containing FunctionCall with only optional fields
|
||||
OUTPUT=$("${DRHARNESS}" -R TestVariantType '[{"name": "test"}]')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: TestVariantType partial FunctionCall - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All variant tests passed!"
|
||||
Executable
+141
@@ -0,0 +1,141 @@
|
||||
#!/bin/bash
|
||||
# test_multimodal_message.sh - Tests for multimodal message construction with -i flag
|
||||
|
||||
# Create a minimal 1x1 PNG file for testing
|
||||
printf '\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53\xDE\x00\x00\x00\x0C\x49\x44\x41\x54\x08\xD7\x63\xF8\xCF\xC0\x00\x00\x03\x01\x01\x00\x18\xDD\x8D\xB4\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82' > "${TEST_TEMP_DIR}/test.png"
|
||||
|
||||
# Test 1: Verify multimodal message format in chat.json with -i and -d flags
|
||||
echo "$(be_done "Response")" | "${DRHARNESS}" \
|
||||
-F "${TEST_TEMP_DIR}/chat.json" \
|
||||
-n \
|
||||
-d \
|
||||
-i "${TEST_TEMP_DIR}/test.png" \
|
||||
-m "describe this image" 2>/dev/null
|
||||
|
||||
# Verify the first message (index 0) is a user message with multimodal content
|
||||
ROLE=$(dr_history_role "${TEST_TEMP_DIR}/chat.json" 0)
|
||||
if [ "$ROLE" != "user" ]; then
|
||||
echo "FAIL: Expected first message role to be 'user', got '${ROLE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify content is an array (multimodal format)
|
||||
CONTENT_TYPE=$(dr_content_type "${TEST_TEMP_DIR}/chat.json" 0)
|
||||
if [ "$CONTENT_TYPE" != "array" ]; then
|
||||
echo "FAIL: Expected content to be an array for multimodal message, got '${CONTENT_TYPE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify first content part is text type
|
||||
TEXT_TYPE=$(dr_content_part_type "${TEST_TEMP_DIR}/chat.json" 0 0)
|
||||
if [ "$TEXT_TYPE" != "text" ]; then
|
||||
echo "FAIL: Expected first content part type to be 'text', got '${TEXT_TYPE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify text content matches the prompt
|
||||
TEXT_CONTENT=$(dr_content_part_text "${TEST_TEMP_DIR}/chat.json" 0 0)
|
||||
if [ "$TEXT_CONTENT" != "describe this image" ]; then
|
||||
echo "FAIL: Expected text content to be 'describe this image', got '${TEXT_CONTENT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify second content part is image_url type
|
||||
IMAGE_TYPE=$(dr_content_part_type "${TEST_TEMP_DIR}/chat.json" 0 1)
|
||||
if [ "$IMAGE_TYPE" != "image_url" ]; then
|
||||
echo "FAIL: Expected second content part type to be 'image_url', got '${IMAGE_TYPE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify image_url has a valid data URI
|
||||
IMAGE_URL=$(dr_content_part_image_url "${TEST_TEMP_DIR}/chat.json" 0 1)
|
||||
if ! echo "$IMAGE_URL" | grep -q "^data:image/png;base64,"; then
|
||||
echo "FAIL: Expected image URL to be a PNG data URI, got '${IMAGE_URL}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify image detail is "auto"
|
||||
IMAGE_DETAIL=$(dr_content_part_image_detail "${TEST_TEMP_DIR}/chat.json" 0 1)
|
||||
if [ "$IMAGE_DETAIL" != "auto" ]; then
|
||||
echo "FAIL: Expected image detail to be 'auto', got '${IMAGE_DETAIL}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 2: Verify multiple images are included
|
||||
printf '\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53\xDE\x00\x00\x00\x0C\x49\x44\x41\x54\x08\xD7\x63\xF8\xCF\xC0\x00\x00\x03\x01\x01\x00\x18\xDD\x8D\xB4\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82' > "${TEST_TEMP_DIR}/test2.png"
|
||||
|
||||
echo "$(be_done "Response 2")" | "${DRHARNESS}" \
|
||||
-F "${TEST_TEMP_DIR}/chat2.json" \
|
||||
-n \
|
||||
-d \
|
||||
-i "${TEST_TEMP_DIR}/test.png" \
|
||||
-i "${TEST_TEMP_DIR}/test2.png" \
|
||||
-m "compare images" 2>/dev/null
|
||||
|
||||
# Verify content array has 3 parts (1 text + 2 images)
|
||||
CONTENT_LENGTH=$(dr_content_parts_count "${TEST_TEMP_DIR}/chat2.json" 0)
|
||||
if [ "$CONTENT_LENGTH" != "3" ]; then
|
||||
echo "FAIL: Expected 3 content parts (1 text + 2 images), got '${CONTENT_LENGTH}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify third content part is also image_url
|
||||
THIRD_TYPE=$(dr_content_part_type "${TEST_TEMP_DIR}/chat2.json" 0 2)
|
||||
if [ "$THIRD_TYPE" != "image_url" ]; then
|
||||
echo "FAIL: Expected third content part type to be 'image_url', got '${THIRD_TYPE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 3: Verify backward compatibility - text-only message when no images
|
||||
echo "$(be_done "Response 3")" | "${DRHARNESS}" \
|
||||
-F "${TEST_TEMP_DIR}/chat3.json" \
|
||||
-n \
|
||||
-d \
|
||||
-m "text only prompt" 2>/dev/null
|
||||
|
||||
# Verify content is a string (not an array) when no images
|
||||
BACKWARD_TYPE=$(dr_content_type "${TEST_TEMP_DIR}/chat3.json" 0)
|
||||
if [ "$BACKWARD_TYPE" != "string" ]; then
|
||||
echo "FAIL: Expected content to be a string for text-only message, got '${BACKWARD_TYPE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BACKWARD_CONTENT=$(dr_content_text "${TEST_TEMP_DIR}/chat3.json" 0)
|
||||
if [ "$BACKWARD_CONTENT" != "text only prompt" ]; then
|
||||
echo "FAIL: Expected text content to be 'text only prompt', got '${BACKWARD_CONTENT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 4: Verify -i works with -t tool filter flag
|
||||
echo "$(be_done "Response with tool filter")" | "${DRHARNESS}" \
|
||||
-F "${TEST_TEMP_DIR}/chat4.json" \
|
||||
-n \
|
||||
-d \
|
||||
-t read_file \
|
||||
-i "${TEST_TEMP_DIR}/test.png" \
|
||||
-m "read and describe" 2>/dev/null
|
||||
|
||||
# Verify multimodal format still works with tool filter
|
||||
TOOL_FILTER_TYPE=$(dr_content_type "${TEST_TEMP_DIR}/chat4.json" 0)
|
||||
if [ "$TOOL_FILTER_TYPE" != "array" ]; then
|
||||
echo "FAIL: Expected multimodal content array with -t flag, got '${TOOL_FILTER_TYPE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 5: Verify -i works with -M model override flag
|
||||
echo "$(be_done "Response with model override")" | "${DRHARNESS}" \
|
||||
-F "${TEST_TEMP_DIR}/chat5.json" \
|
||||
-n \
|
||||
-d \
|
||||
-M "test-model" \
|
||||
-i "${TEST_TEMP_DIR}/test.png" \
|
||||
-m "test model override" 2>/dev/null
|
||||
|
||||
# Verify multimodal format still works with model override
|
||||
MODEL_OVERRIDE_TYPE=$(dr_content_type "${TEST_TEMP_DIR}/chat5.json" 0)
|
||||
if [ "$MODEL_OVERRIDE_TYPE" != "array" ]; then
|
||||
echo "FAIL: Expected multimodal content array with -M flag, got '${MODEL_OVERRIDE_TYPE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All multimodal message tests passed"
|
||||
Executable
+192
@@ -0,0 +1,192 @@
|
||||
#!/bin/bash
|
||||
# test_multimodal_roundtrip.sh - Tests for chat file persistence round-trip with multimodal messages
|
||||
|
||||
# Create a minimal 1x1 PNG file for testing
|
||||
printf '\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53\xDE\x00\x00\x00\x0C\x49\x44\x41\x54\x08\xD7\x63\xF8\xCF\xC0\x00\x00\x03\x01\x01\x00\x18\xDD\x8D\xB4\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82' > "${TEST_TEMP_DIR}/test.png"
|
||||
|
||||
# =============================================================================
|
||||
# Test 1: Round-trip serialization preserves multimodal content
|
||||
# =============================================================================
|
||||
|
||||
# Step 1: Create a chat with multimodal message
|
||||
echo "$(be_done "First response")" | "${DRHARNESS}" \
|
||||
-F "${TEST_TEMP_DIR}/chat.json" \
|
||||
-n \
|
||||
-d \
|
||||
-i "${TEST_TEMP_DIR}/test.png" \
|
||||
-m "describe this image" 2>/dev/null
|
||||
|
||||
# Verify the multimodal message was serialized correctly
|
||||
CONTENT_TYPE=$(jq -r '.history[0].content | type' "${TEST_TEMP_DIR}/chat.json")
|
||||
if [ "$CONTENT_TYPE" != "array" ]; then
|
||||
echo "FAIL: Expected content to be array, got '${CONTENT_TYPE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Capture the original image URL for comparison
|
||||
ORIGINAL_IMAGE_URL=$(jq -r '.history[0].content[1].image_url.url' "${TEST_TEMP_DIR}/chat.json")
|
||||
if ! echo "$ORIGINAL_IMAGE_URL" | grep -q "^data:image/png;base64,"; then
|
||||
echo "FAIL: Expected image URL to be PNG data URI"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 2: Resume the chat and continue the conversation
|
||||
echo "$(be_done "Second response")" | "${DRHARNESS}" \
|
||||
-F "${TEST_TEMP_DIR}/chat.json" \
|
||||
-d \
|
||||
-m "continue conversation" 2>/dev/null
|
||||
|
||||
# =============================================================================
|
||||
# Test 2: Verify round-trip preserves multimodal content after resume
|
||||
# =============================================================================
|
||||
|
||||
# The first message (index 0) should still be the user multimodal message
|
||||
ROLE=$(jq -r '.history[0].role' "${TEST_TEMP_DIR}/chat.json")
|
||||
if [ "$ROLE" != "user" ]; then
|
||||
echo "FAIL: Expected first message role to be 'user', got '${ROLE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Content should still be an array
|
||||
CONTENT_TYPE=$(jq -r '.history[0].content | type' "${TEST_TEMP_DIR}/chat.json")
|
||||
if [ "$CONTENT_TYPE" != "array" ]; then
|
||||
echo "FAIL: After resume, expected content to be array, got '${CONTENT_TYPE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify first content part is still text type
|
||||
TEXT_TYPE=$(jq -r '.history[0].content[0].type' "${TEST_TEMP_DIR}/chat.json")
|
||||
if [ "$TEXT_TYPE" != "text" ]; then
|
||||
echo "FAIL: Expected first content part type to be 'text', got '${TEXT_TYPE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify text content is preserved
|
||||
TEXT_CONTENT=$(jq -r '.history[0].content[0].text' "${TEST_TEMP_DIR}/chat.json")
|
||||
if [ "$TEXT_CONTENT" != "describe this image" ]; then
|
||||
echo "FAIL: Expected text content 'describe this image', got '${TEXT_CONTENT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify second content part is still image_url type
|
||||
IMAGE_TYPE=$(jq -r '.history[0].content[1].type' "${TEST_TEMP_DIR}/chat.json")
|
||||
if [ "$IMAGE_TYPE" != "image_url" ]; then
|
||||
echo "FAIL: Expected second content part type to be 'image_url', got '${IMAGE_TYPE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Test 3: Image data URI is preserved in round-trip
|
||||
# =============================================================================
|
||||
|
||||
RESUMED_IMAGE_URL=$(jq -r '.history[0].content[1].image_url.url' "${TEST_TEMP_DIR}/chat.json")
|
||||
if [ "$ORIGINAL_IMAGE_URL" != "$RESUMED_IMAGE_URL" ]; then
|
||||
echo "FAIL: Image data URI not preserved in round-trip"
|
||||
echo "Original: $ORIGINAL_IMAGE_URL"
|
||||
echo "Resumed: $RESUMED_IMAGE_URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify image detail is preserved
|
||||
IMAGE_DETAIL=$(jq -r '.history[0].content[1].image_url.detail' "${TEST_TEMP_DIR}/chat.json")
|
||||
if [ "$IMAGE_DETAIL" != "auto" ]; then
|
||||
echo "FAIL: Expected image detail 'auto', got '${IMAGE_DETAIL}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Test 4: Text-only message round-trip (backward compatibility)
|
||||
# =============================================================================
|
||||
|
||||
# Create a new chat with text-only message
|
||||
echo "$(be_done "Text response")" | "${DRHARNESS}" \
|
||||
-F "${TEST_TEMP_DIR}/text_chat.json" \
|
||||
-n \
|
||||
-d \
|
||||
-m "text only prompt" 2>/dev/null
|
||||
|
||||
# Verify text-only content is a string
|
||||
TEXT_CONTENT_TYPE=$(jq -r '.history[0].content | type' "${TEST_TEMP_DIR}/text_chat.json")
|
||||
if [ "$TEXT_CONTENT_TYPE" != "string" ]; then
|
||||
echo "FAIL: Expected text-only content to be string, got '${TEXT_CONTENT_TYPE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Resume and verify text-only message is preserved
|
||||
echo "$(be_done "More text")" | "${DRHARNESS}" \
|
||||
-F "${TEST_TEMP_DIR}/text_chat.json" \
|
||||
-d \
|
||||
-m "second text prompt" 2>/dev/null
|
||||
|
||||
# Verify first message content is still a string
|
||||
RESUMED_TEXT_TYPE=$(jq -r '.history[0].content | type' "${TEST_TEMP_DIR}/text_chat.json")
|
||||
if [ "$RESUMED_TEXT_TYPE" != "string" ]; then
|
||||
echo "FAIL: After resume, expected text-only content to be string, got '${RESUMED_TEXT_TYPE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RESUMED_TEXT=$(jq -r '.history[0].content' "${TEST_TEMP_DIR}/text_chat.json")
|
||||
if [ "$RESUMED_TEXT" != "text only prompt" ]; then
|
||||
echo "FAIL: Text content not preserved, expected 'text only prompt', got '${RESUMED_TEXT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Test 5: Mixed content types in same chat history
|
||||
# =============================================================================
|
||||
|
||||
# Create chat with multimodal message first
|
||||
echo "$(be_done "Response to multimodal")" | "${DRHARNESS}" \
|
||||
-F "${TEST_TEMP_DIR}/mixed_chat.json" \
|
||||
-n \
|
||||
-d \
|
||||
-i "${TEST_TEMP_DIR}/test.png" \
|
||||
-m "first multimodal" 2>/dev/null
|
||||
|
||||
# Resume and add text-only message
|
||||
echo "$(be_done "Response to text")" | "${DRHARNESS}" \
|
||||
-F "${TEST_TEMP_DIR}/mixed_chat.json" \
|
||||
-d \
|
||||
-m "second text only" 2>/dev/null
|
||||
|
||||
# Verify first message (multimodal) is array
|
||||
FIRST_CONTENT_TYPE=$(jq -r '.history[0].content | type' "${TEST_TEMP_DIR}/mixed_chat.json")
|
||||
if [ "$FIRST_CONTENT_TYPE" != "array" ]; then
|
||||
echo "FAIL: Mixed chat: first message should be array, got '${FIRST_CONTENT_TYPE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify third message (user text-only) is string (index 2: user, assistant, user)
|
||||
THIRD_CONTENT_TYPE=$(jq -r '.history[2].content | type' "${TEST_TEMP_DIR}/mixed_chat.json")
|
||||
if [ "$THIRD_CONTENT_TYPE" != "string" ]; then
|
||||
echo "FAIL: Mixed chat: third message should be string, got '${THIRD_CONTENT_TYPE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Resume again and verify both types still preserved
|
||||
echo "$(be_done "Third response")" | "${DRHARNESS}" \
|
||||
-F "${TEST_TEMP_DIR}/mixed_chat.json" \
|
||||
-d \
|
||||
-m "third prompt" 2>/dev/null
|
||||
|
||||
# Verify after second resume
|
||||
FINAL_FIRST_TYPE=$(jq -r '.history[0].content | type' "${TEST_TEMP_DIR}/mixed_chat.json")
|
||||
if [ "$FINAL_FIRST_TYPE" != "array" ]; then
|
||||
echo "FAIL: After second resume: first message should still be array"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FINAL_THIRD_TYPE=$(jq -r '.history[2].content | type' "${TEST_TEMP_DIR}/mixed_chat.json")
|
||||
if [ "$FINAL_THIRD_TYPE" != "string" ]; then
|
||||
echo "FAIL: After second resume: third message should still be string"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify the multimodal content data is still intact
|
||||
FINAL_IMAGE_URL=$(jq -r '.history[0].content[1].image_url.url' "${TEST_TEMP_DIR}/mixed_chat.json")
|
||||
if ! echo "$FINAL_IMAGE_URL" | grep -q "^data:image/png;base64,"; then
|
||||
echo "FAIL: After second resume: image URL corrupted"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All multimodal round-trip tests passed"
|
||||
Executable
+94
@@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
# test_print_history_multimodal.sh - Tests for print_history (-p flag) with multimodal content
|
||||
|
||||
# Create a minimal 1x1 PNG file for testing
|
||||
printf '\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53\xDE\x00\x00\x00\x0C\x49\x44\x41\x54\x08\xD7\x63\xF8\xCF\xC0\x00\x00\x03\x01\x01\x00\x18\xDD\x8D\xB4\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82' > "${TEST_TEMP_DIR}/test.png"
|
||||
|
||||
# =============================================================================
|
||||
# Test 1: Verify -p flag displays text content parts in multimodal messages
|
||||
# =============================================================================
|
||||
|
||||
# Create a chat with multimodal message
|
||||
echo "$(be_done "Response")" | "${DRHARNESS}" \
|
||||
-F "${TEST_TEMP_DIR}/chat.json" \
|
||||
-n \
|
||||
-d \
|
||||
-i "${TEST_TEMP_DIR}/test.png" \
|
||||
-m "describe this image" 2>/dev/null
|
||||
|
||||
# Print history using -p flag
|
||||
OUTPUT=$("${DRHARNESS}" -p "${TEST_TEMP_DIR}/chat.json" 2>/dev/null)
|
||||
|
||||
# Verify text content is displayed
|
||||
if ! echo "$OUTPUT" | grep -q "describe this image"; then
|
||||
echo "FAIL: Expected 'describe this image' in output, got:"
|
||||
echo "$OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Test 2: Verify -p flag indicates presence of image_url content parts
|
||||
# =============================================================================
|
||||
|
||||
# Verify [Image] indicator is present
|
||||
if ! echo "$OUTPUT" | grep -q "\[Image\]"; then
|
||||
echo "FAIL: Expected '[Image]' indicator in output, got:"
|
||||
echo "$OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Test 3: Verify text-only messages still display correctly
|
||||
# =============================================================================
|
||||
|
||||
# Create a chat with text-only message
|
||||
echo "$(be_done "Text response")" | "${DRHARNESS}" \
|
||||
-F "${TEST_TEMP_DIR}/chat_text.json" \
|
||||
-n \
|
||||
-d \
|
||||
-m "text only prompt" 2>/dev/null
|
||||
|
||||
# Print history using -p flag
|
||||
TEXT_OUTPUT=$("${DRHARNESS}" -p "${TEST_TEMP_DIR}/chat_text.json" 2>/dev/null)
|
||||
|
||||
# Verify text content is displayed
|
||||
if ! echo "$TEXT_OUTPUT" | grep -q "text only prompt"; then
|
||||
echo "FAIL: Expected 'text only prompt' in output, got:"
|
||||
echo "$TEXT_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify no [Image] indicator for text-only message
|
||||
if echo "$TEXT_OUTPUT" | grep -q "\[Image\]"; then
|
||||
echo "FAIL: Did not expect '[Image]' in text-only output, got:"
|
||||
echo "$TEXT_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Test 4: Verify multiple images are all indicated
|
||||
# =============================================================================
|
||||
|
||||
# Create second image file
|
||||
printf '\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53\xDE\x00\x00\x00\x0C\x49\x44\x41\x54\x08\xD7\x63\xF8\xCF\xC0\x00\x00\x03\x01\x01\x00\x18\xDD\x8D\xB4\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82' > "${TEST_TEMP_DIR}/test2.png"
|
||||
|
||||
echo "$(be_done "Response 2")" | "${DRHARNESS}" \
|
||||
-F "${TEST_TEMP_DIR}/chat_multi.json" \
|
||||
-n \
|
||||
-d \
|
||||
-i "${TEST_TEMP_DIR}/test.png" \
|
||||
-i "${TEST_TEMP_DIR}/test2.png" \
|
||||
-m "compare images" 2>/dev/null
|
||||
|
||||
# Print history using -p flag
|
||||
MULTI_OUTPUT=$("${DRHARNESS}" -p "${TEST_TEMP_DIR}/chat_multi.json" 2>/dev/null)
|
||||
|
||||
# Count [Image] occurrences - should be 2
|
||||
IMAGE_COUNT=$(echo "$MULTI_OUTPUT" | grep -o "\[Image\]" | wc -l)
|
||||
if [ "$IMAGE_COUNT" != "2" ]; then
|
||||
echo "FAIL: Expected 2 '[Image]' indicators for 2 images, got ${IMAGE_COUNT}"
|
||||
echo "$MULTI_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All print_history multimodal tests passed"
|
||||
@@ -5,7 +5,7 @@ test_name="basic_messages"
|
||||
|
||||
json=$(create_messages_json user "hello" assistant "hi there")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -B "$json")
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$json")
|
||||
|
||||
# Check header exists
|
||||
if ! echo "$OUTPUT" | grep -q "# History Rebase Editor"; then
|
||||
|
||||
@@ -5,7 +5,7 @@ test_name="empty_message"
|
||||
|
||||
json=$(create_messages_json user "")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -B "$json")
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$json")
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "pick 0 \[user\] (empty)"; then
|
||||
echo "FAIL: $test_name - should show (empty) for empty content"
|
||||
|
||||
@@ -5,7 +5,7 @@ test_name="header_comments"
|
||||
|
||||
json=$(create_messages_json user "test")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -B "$json")
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$json")
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "pick - keep the message unchanged"; then
|
||||
echo "FAIL: $test_name - missing pick explanation"
|
||||
|
||||
@@ -5,7 +5,7 @@ test_name="message_indices"
|
||||
|
||||
json=$(create_messages_json user "msg1" assistant "msg2" tool "msg3")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -B "$json")
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$json")
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "pick 0 \[user\] msg1"; then
|
||||
echo "FAIL: $test_name - missing index 0"
|
||||
|
||||
@@ -7,7 +7,7 @@ json=$(create_messages_json user "first line
|
||||
second line
|
||||
third line")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -B "$json")
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$json")
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "pick 0 \[user\] first line"; then
|
||||
echo "FAIL: $test_name - should show only first line"
|
||||
|
||||
@@ -5,7 +5,7 @@ test_name="multiple_tool_calls"
|
||||
|
||||
json=$(create_messages_json assistant "" tool_call "call_1" "read_file" "" tool_call "call_2" "write_file" "")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -B "$json")
|
||||
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 ..."
|
||||
|
||||
@@ -5,7 +5,7 @@ test_name="scissors_line"
|
||||
|
||||
json=$(create_messages_json user "test")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -B "$json")
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$json")
|
||||
|
||||
# Check for scissors line
|
||||
if ! echo "$OUTPUT" | grep -q -- "-- >8 --"; then
|
||||
|
||||
@@ -5,7 +5,7 @@ test_name="special_characters"
|
||||
|
||||
json=$(create_messages_json user 'hello "quotes" and $dollar' assistant 'backslash\here')
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -B "$json")
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$json")
|
||||
|
||||
# Should handle quotes and special characters
|
||||
if ! echo "$OUTPUT" | grep -q "pick 0 \[user\]"; then
|
||||
|
||||
@@ -5,7 +5,7 @@ test_name="tool_call_display"
|
||||
|
||||
json=$(create_messages_json assistant "" tool_call "call_1" "run_shell" "")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -B "$json")
|
||||
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"
|
||||
|
||||
@@ -5,7 +5,7 @@ test_name="tool_message"
|
||||
|
||||
json=$(create_messages_json tool "tool result content")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -B "$json")
|
||||
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"
|
||||
|
||||
@@ -5,7 +5,7 @@ test_name="unicode_characters"
|
||||
|
||||
json=$(create_messages_json user "こんにちは" assistant "👋 hello 世界")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -B "$json")
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$json")
|
||||
|
||||
# Should preserve unicode characters
|
||||
if ! echo "$OUTPUT" | grep -q "pick 0 \[user\] こんにちは"; then
|
||||
|
||||
@@ -7,7 +7,7 @@ test_name="very_long_content"
|
||||
long_content=$(printf 'x%.0s' {1..1000})
|
||||
json=$(create_messages_json user "$long_content")
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -B "$json")
|
||||
OUTPUT=$("${DRHARNESS}" -Z "$json")
|
||||
|
||||
# Should handle long content without crashing
|
||||
if ! echo "$OUTPUT" | grep -q "pick 0 \[user\]"; then
|
||||
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
# test_tool_read_multi_modal.sh - Tests for read_multi_modal tool
|
||||
|
||||
# Create a minimal 1x1 PNG file for testing
|
||||
printf '\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53\xDE\x00\x00\x00\x0C\x49\x44\x41\x54\x08\xD7\x63\xF8\xCF\xC0\x00\x00\x03\x01\x01\x00\x18\xDD\x8D\xB4\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82' > test_image.png
|
||||
|
||||
# Test 1: Successfully read a valid image file
|
||||
# Mock response: ask for read_multi_modal
|
||||
tool_call read_multi_modal '{"path":"test_image.png"}' >> mock_responses.txt
|
||||
be_done 'Read complete' >> mock_responses.txt
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -F chat.json -n -m "Read image" -t read_multi_modal -d < "mock_responses.txt" 2>/dev/null)
|
||||
|
||||
if [ "${OUTPUT}" != "Read complete" ]; then
|
||||
echo "FAIL: read_multi_modal tool - expected 'Read complete', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify tool result is stored as an array (Content with vector<ContentPart>)
|
||||
CONTENT_TYPE=$(dr_content_type chat.json 2)
|
||||
if [ "$CONTENT_TYPE" != "array" ]; then
|
||||
echo "FAIL: Expected tool result content to be an array, got '${CONTENT_TYPE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify the content part type is image_url
|
||||
PART_TYPE=$(dr_content_part_type chat.json 2 0)
|
||||
if [ "$PART_TYPE" != "image_url" ]; then
|
||||
echo "FAIL: Expected content part type to be 'image_url', got '${PART_TYPE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify the image URL is a valid PNG data URI
|
||||
IMAGE_URL=$(dr_content_part_image_url chat.json 2 0)
|
||||
if ! echo "$IMAGE_URL" | grep -q "^data:image/png;base64,"; then
|
||||
echo "FAIL: Expected image URL to be PNG data URI, got '${IMAGE_URL}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 2: Error handling for non-existent file
|
||||
rm -f chat.json
|
||||
tool_call read_multi_modal '{"path":"nonexistent_image.png"}' >> mock_responses2.txt
|
||||
be_done 'Error handled' >> mock_responses2.txt
|
||||
|
||||
"${DRHARNESS}" -F chat.json -n -m "Read nonexistent" -t read_multi_modal -d < "mock_responses2.txt" >/dev/null 2>&1
|
||||
|
||||
# Verify tool result is a string error message
|
||||
CONTENT_TYPE=$(dr_content_type chat.json 2)
|
||||
if [ "$CONTENT_TYPE" != "string" ]; then
|
||||
echo "FAIL: Expected error content to be a string, got '${CONTENT_TYPE}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RESULT=$(dr_result chat.json 2)
|
||||
if ! echo "$RESULT" | grep -q "Failed to read image"; then
|
||||
echo "FAIL: Expected error message about failed image read, got '${RESULT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 3: Read a JPG file (different MIME type)
|
||||
printf '\xFF\xD8\xFF\xE0\x00\x10JFIF' > test_image.jpg
|
||||
|
||||
rm -f chat.json
|
||||
tool_call read_multi_modal '{"path":"test_image.jpg"}' >> mock_responses3.txt
|
||||
be_done 'JPG read complete' >> mock_responses3.txt
|
||||
|
||||
"${DRHARNESS}" -F chat.json -n -m "Read JPG" -t read_multi_modal -d < "mock_responses3.txt" >/dev/null 2>&1
|
||||
|
||||
# Verify the image URL is a valid JPEG data URI
|
||||
IMAGE_URL=$(dr_content_part_image_url chat.json 2 0)
|
||||
if ! echo "$IMAGE_URL" | grep -q "^data:image/jpeg;base64,"; then
|
||||
echo "FAIL: Expected image URL to be JPEG data URI, got '${IMAGE_URL}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: tool_read_multi_modal"
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "tool.hpp"
|
||||
#include "read_multi_modal.hpp"
|
||||
#include "json_types.hpp"
|
||||
#include "base64_util.hpp"
|
||||
|
||||
|
||||
|
||||
Tool make_read_multi_modal() {
|
||||
return Tool{
|
||||
.name = "read_multi_modal",
|
||||
.description = ""
|
||||
"Reads multi modal files (images, audio, etc)\n",
|
||||
|
||||
.parameters_schema = {
|
||||
{"type", "object"},
|
||||
{"properties", {
|
||||
{"path", {
|
||||
{"type", "string"},
|
||||
{"description", "Absolute or relative path to multi modal file to read"}
|
||||
}},
|
||||
}},
|
||||
{"required", {"path"}}
|
||||
},
|
||||
.execute = create_tool_execute<ReadFileToolParams>([](const ReadFileToolParams& params) -> Content {
|
||||
std::vector<ContentPart> ret{};
|
||||
|
||||
ContentPart image_part;
|
||||
image_part.type = "image_url";
|
||||
ImageUrl img_url;
|
||||
auto uri = read_image_as_data_uri(params.path);
|
||||
if (!uri)
|
||||
return "Failed to read image " + uri.error();
|
||||
|
||||
img_url.url = *uri;
|
||||
image_part.image_url = img_url;
|
||||
ret.push_back(image_part);
|
||||
|
||||
return ret;
|
||||
})
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
#include "tool.hpp"
|
||||
|
||||
Tool make_read_multi_modal();
|
||||
Reference in New Issue
Block a user