Files
hal9000 02975ddfd4 Fix: Use helper functions in test_harness_config.sh
- Added dr_config_base_url and dr_config_model helper functions to run_test.sh
- Updated test_harness_config.sh to use be_done() and helper functions instead of inline JSON
2026-08-04 20:20:10 +00:00

509 lines
14 KiB
Bash
Executable File

#!/bin/bash
# run_test.sh - Common test runner with setup and teardown logic
# Usage: run_test.sh <test_script.sh>
set -eu
set -x
function check_tool_call() {
chat_file="$1"
entry="$2"
expected_out="$3"
if [ "$(cat "$chat_file" | jq --arg e "$entry" -r '.history[$e | tonumber].role')" != "tool" ]; then
echo "Expected $entry chat to be a tool call result"
exit 1
fi
diff -u "$expected_out" <(cat "$chat_file" | jq --arg e "$entry" -r '.history[$e | tonumber].content')
}
# Generates a message JSON blob
# Usage: message [options]
# Options:
# --content TEXT Set message content
# --tool-call NAME ARGS Add a tool call (name and JSON args)
# --reasoning TEXT Add reasoning field
# --reasoning-content TEXT Add reasoning_content field
function message() {
local content=""
local reasoning=""
local reasoning_content=""
local has_tool_call=0
local tool_name=""
local tool_args=""
while [[ $# -gt 0 ]]; do
case "$1" in
--content)
content="$2"
shift 2
;;
--tool-call)
has_tool_call=1
tool_name="$2"
tool_args="$3"
shift 3
;;
--reasoning)
reasoning="$2"
shift 2
;;
--reasoning-content)
reasoning_content="$2"
shift 2
;;
esac
done
local msg='{"role":"assistant"}'
if [[ $has_tool_call -eq 1 ]]; then
tool_args=$(echo "$tool_args" | jq -R)
msg=$(echo "$msg" | jq \
--arg name "$tool_name" \
--argjson args "$tool_args" \
'. + {
"tool_calls": [{"index":0, "id": "call_1", "type": "function", "function": {"name": $name, "arguments": $args}}],
"content": null
}')
else
msg=$(echo "$msg" | jq \
--arg content "$content" \
--arg reasoning "$reasoning" \
--arg reasoning_content "$reasoning_content" \
'. + {"content": $content} | if $reasoning != "" then . + {"reasoning": $reasoning} else . end | if $reasoning_content != "" then . + {"reasoning_content": $reasoning_content} else . end')
fi
echo "$msg" | jq -c
}
function api_response() {
local message_json="$1"
local finish_reason="$2"
echo "{\"choices\":[{\"delta\":${message_json},\"finish_reason\":\"${finish_reason}\"}],\"usage\":{\"completion_tokens\":336,\"prompt_tokens\":14,\"total_tokens\":350}}"
}
function tool_call() {
local name="$1"
local args="$2"
local msg
msg=$(message --tool-call "$name" "$args")
api_response "$msg" "tool_calls"
}
function tool_call_raw() {
local name="$1"
local args="$2"
local msg
msg=$(message --tool-call "$name" "$args")
api_response "$msg" "tool_calls"
}
function be_done() {
local content="$1"
local msg
msg=$(message --content "$content")
api_response "$msg" "stop"
}
function reasoning_step() {
local content="$1"
local msg
msg=$(message --reasoning "$content")
api_response "$msg" "stop"
}
function reasoning_content_step() {
local content="$1"
local msg
msg=$(message --reasoning-content "$content")
api_response "$msg" "stop"
}
export -f check_tool_call
export -f message
export -f tool_call
export -f tool_call_raw
export -f api_response
export -f be_done
export -f reasoning_step
export -f reasoning_content_step
function history_length() {
local chat_file="$1"
jq '.history | length' "$chat_file"
}
export -f history_length
function dr_result() {
local chat_file="$1"
local entry="$2"
jq -r --arg e "$entry" '.history[$e | tonumber].content' "$chat_file" 2>/dev/null || echo ""
}
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
# Build history array from role/content pairs using jq for proper escaping
local history_json="[]"
while [ $# -gt 0 ]; do
local role="$1"
local content="$2"
shift 2
history_json=$(echo "$history_json" | jq --arg r "$role" --arg c "$content" '. + [{"role": $r, "content": $c}]')
done
# Generate the complete state file
echo "$history_json" | jq '{
"history": .,
"tools": [],
"config": {
"max_tool_iterations": 16,
"verbose": false,
"depth": 0
}
}' > "$file"
}
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
# Get base_url from config in chat file
function dr_config_base_url() {
local chat_file="$1"
jq -r '.config.base_url' "$chat_file"
}
export -f dr_config_base_url
# Get model from config in chat file
function dr_config_model() {
local chat_file="$1"
jq -r '.config.model' "$chat_file"
}
export -f dr_config_model
# Resolve paths relative to this script
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_ROOT=$(cd "${SCRIPT_DIR}/.." && pwd)
DRHARNESS="${PWD}/drharness"
MOCK_EDITOR="${PROJECT_ROOT}/tests/mock_editor.sh"
# Validate drharness binary exists
if [ ! -f "${DRHARNESS}" ]; then
echo "ERROR: drharness not found at ${DRHARNESS}" >&2
echo "Run 'cmake --build build' first." >&2
exit 1
fi
# Validate arguments
if [ $# -lt 1 ]; then
echo "Usage: run_test.sh <test_script.sh>" >&2
exit 1
fi
TEST_SCRIPT="$1"
TEST_TEMP_DIR=$(mktemp -d /tmp/drharness_test_XXXXXX)
# Export variables for the test script
export DRHARNESS
export TEST_TEMP_DIR
export MOCK_EDITOR
# Cleanup function
cleanup() {
rm -rf "${TEST_TEMP_DIR}"
}
trap cleanup EXIT
# Change to isolation directory and run the test
cd "${TEST_TEMP_DIR}"
bash -xeu "${TEST_SCRIPT}"
exit $?