Files
1jehuang--jcode/tests/test_injection_thorough.py
T
wehub-resource-sync a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:34 +08:00

340 lines
11 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Thorough testing of soft interrupt injection fix.
Tests that:
1. With Claude provider: injected messages appear AFTER tool_results
2. With OpenAI provider: injected messages appear AFTER tool_results
3. Multiple tool calls: injection happens after ALL results
4. Urgent interrupts: tool_results are added for skipped tools
5. No API errors about tool_use/tool_result pairing
Run with: python tests/test_injection_thorough.py
"""
import socket
import json
import time
import os
import sys
import re
RUNTIME_DIR = os.environ.get("XDG_RUNTIME_DIR") or f"/run/user/{os.getuid()}"
SOCKET_PATH = os.path.join(RUNTIME_DIR, "jcode-debug.sock")
def send_cmd(sock, cmd, session_id=None, timeout=120):
"""Send a debug command and get the response."""
req = {"type": "debug_command", "id": 1, "command": cmd}
if session_id:
req["session_id"] = session_id
sock.send((json.dumps(req) + '\n').encode())
sock.settimeout(timeout)
data = b""
start = time.time()
while time.time() - start < timeout:
try:
chunk = sock.recv(65536)
if not chunk:
break
data += chunk
try:
return json.loads(data.decode())
except json.JSONDecodeError:
continue
except socket.timeout:
break
return json.loads(data.decode()) if data else None
def check_history_order(history):
"""
Check that no user text message appears between tool_use and tool_result.
Returns (is_valid, error_message)
"""
waiting_for_results = set() # tool_use IDs that need results
for i, msg in enumerate(history):
role = msg.get('role', '')
content = msg.get('content', '')
# Check for tool_use in assistant message
if role == 'assistant':
# Look for tool_use patterns like [tool: bash] or tool calls
tool_matches = re.findall(r'\[tool: (\w+)\]', content)
if tool_matches:
for tool_name in tool_matches:
waiting_for_results.add(f"tool_{i}_{tool_name}")
# Check for tool_result
if role == 'tool' or (role == 'user' and '[result:' in content):
# A tool result was found, clear one waiting
if waiting_for_results:
waiting_for_results.pop()
# Check for user text while waiting for results
if role == 'user' and waiting_for_results:
# Is this a tool result or actual user text?
if '[result:' not in content and 'tool' not in content.lower():
# This is user text between tool_use and tool_result!
return False, f"User text '{content[:50]}...' found while waiting for tool_result for: {waiting_for_results}"
return True, None
def test_injection_with_provider(provider_name, session_id, sock):
"""Test injection with a specific provider."""
print(f"\n--- Testing with {provider_name} provider ---")
# Switch to the provider
if provider_name == "openai":
result = send_cmd(sock, "set_provider:openai", session_id)
if not result or not result.get('ok'):
print(f" Could not switch to OpenAI: {result}")
print(f" Skipping OpenAI tests (may not be configured)")
return True # Skip is not failure
# Queue a soft interrupt
print("1. Queueing soft interrupt...")
result = send_cmd(sock, "queue_interrupt:This is an interrupt during tools", session_id)
if not result:
print(" Failed to queue interrupt")
return False
print(f" Queued: {result.get('output', 'OK')}")
# Send a message that will trigger tool use
print("2. Sending message that triggers tool use...")
result = send_cmd(sock, "message:Run the bash command: echo 'hello from test'", session_id, timeout=180)
if not result:
print(" No response (timeout)")
return False
if not result.get('ok'):
error = result.get('error', '')
# Check for the specific error we're trying to prevent
if 'tool_use' in error.lower() and 'tool_result' in error.lower():
print(f" FAIL: Got tool_use/tool_result pairing error!")
print(f" Error: {error}")
return False
print(f" Response error (may be expected): {error[:100]}...")
print(" Response received")
# Check history
print("3. Checking message history order...")
result = send_cmd(sock, "history", session_id)
if not result or not result.get('ok'):
print(f" Failed to get history: {result}")
return False
history = json.loads(result['output'])
print(f" Found {len(history)} messages")
is_valid, error_msg = check_history_order(history)
if not is_valid:
print(f" FAIL: {error_msg}")
return False
print(f" ✓ History order is valid for {provider_name}")
return True
def test_multiple_tools():
"""Test injection when multiple tools are called."""
print("\n" + "=" * 60)
print("Test: Multiple tool calls")
print("=" * 60)
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
sock.connect(SOCKET_PATH)
except (FileNotFoundError, ConnectionRefusedError) as e:
print(f"ERROR: Cannot connect to debug socket: {e}")
return False
try:
# Create session
result = send_cmd(sock, "create_session:/tmp/multi-tool-test")
if not result or not result.get('ok'):
print(f"Failed to create session: {result}")
return False
session_id = json.loads(result['output'])['session_id']
print(f"Session ID: {session_id}")
# Queue interrupt
print("\n1. Queueing interrupt before multiple tool calls...")
send_cmd(sock, "queue_interrupt:Interrupting during multiple tools", session_id)
# Request multiple tool calls
print("2. Requesting multiple bash commands...")
result = send_cmd(sock,
"message:Please run these bash commands one at a time: echo first, echo second, echo third",
session_id, timeout=180)
if result and not result.get('ok'):
error = result.get('error', '')
if 'tool_use' in error.lower() and 'tool_result' in error.lower():
print(f" FAIL: Tool pairing error with multiple tools!")
return False
# Check history
print("3. Verifying history order...")
result = send_cmd(sock, "history", session_id)
if result and result.get('ok'):
history = json.loads(result['output'])
is_valid, error_msg = check_history_order(history)
if not is_valid:
print(f" FAIL: {error_msg}")
return False
print(" ✓ History order is valid")
send_cmd(sock, f"destroy_session:{session_id}")
print("\n" + "=" * 60)
print("TEST PASSED: Multiple tool calls handled correctly")
print("=" * 60)
return True
finally:
sock.close()
def test_urgent_interrupt():
"""Test urgent interrupt (should skip remaining tools with stub results)."""
print("\n" + "=" * 60)
print("Test: Urgent interrupt")
print("=" * 60)
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
sock.connect(SOCKET_PATH)
except (FileNotFoundError, ConnectionRefusedError) as e:
print(f"ERROR: Cannot connect to debug socket: {e}")
return False
try:
result = send_cmd(sock, "create_session:/tmp/urgent-test")
if not result or not result.get('ok'):
return False
session_id = json.loads(result['output'])['session_id']
print(f"Session ID: {session_id}")
# Queue URGENT interrupt
print("\n1. Queueing URGENT interrupt...")
send_cmd(sock, "queue_interrupt_urgent:STOP! Cancel remaining tools!", session_id)
# Request tool calls
print("2. Requesting tool calls...")
result = send_cmd(sock,
"message:Run these commands: echo a, echo b, echo c",
session_id, timeout=180)
if result and not result.get('ok'):
error = result.get('error', '')
if 'tool_use' in error.lower() and 'tool_result' in error.lower():
print(f" FAIL: Tool pairing error with urgent interrupt!")
return False
# Check that skipped tools have results
print("3. Checking that skipped tools have results...")
result = send_cmd(sock, "history", session_id)
if result and result.get('ok'):
history = json.loads(result['output'])
is_valid, error_msg = check_history_order(history)
if not is_valid:
print(f" FAIL: {error_msg}")
return False
# Look for skip messages
has_skip = any('skip' in str(msg.get('content', '')).lower() for msg in history)
if has_skip:
print(" ✓ Found skip message (tools were interrupted)")
else:
print(" (No skip message - tools may have completed before interrupt)")
send_cmd(sock, f"destroy_session:{session_id}")
print("\n" + "=" * 60)
print("TEST PASSED: Urgent interrupt handled correctly")
print("=" * 60)
return True
finally:
sock.close()
def test_both_providers():
"""Test injection with both Claude and OpenAI providers."""
print("\n" + "=" * 60)
print("Test: Both providers")
print("=" * 60)
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
sock.connect(SOCKET_PATH)
except (FileNotFoundError, ConnectionRefusedError) as e:
print(f"ERROR: Cannot connect to debug socket: {e}")
return False
try:
result = send_cmd(sock, "create_session:/tmp/provider-test")
if not result or not result.get('ok'):
return False
session_id = json.loads(result['output'])['session_id']
print(f"Session ID: {session_id}")
all_passed = True
# Test Claude (default)
if not test_injection_with_provider("claude", session_id, sock):
all_passed = False
# Test OpenAI
if not test_injection_with_provider("openai", session_id, sock):
all_passed = False
send_cmd(sock, f"destroy_session:{session_id}")
if all_passed:
print("\n" + "=" * 60)
print("TEST PASSED: Both providers work correctly")
print("=" * 60)
return all_passed
finally:
sock.close()
def main():
print("Thorough Soft Interrupt Injection Tests")
print("=" * 60)
print(f"Using socket: {SOCKET_PATH}")
print()
# Check socket exists
if not os.path.exists(SOCKET_PATH):
print(f"ERROR: Socket not found at {SOCKET_PATH}")
print("Make sure jcode is running with debug control enabled.")
return 1
all_passed = True
# Test 1: Multiple tool calls
if not test_multiple_tools():
all_passed = False
# Test 2: Urgent interrupt
if not test_urgent_interrupt():
all_passed = False
# Test 3: Both providers (if available)
if not test_both_providers():
all_passed = False
print()
if all_passed:
print("=" * 60)
print("✓ ALL TESTS PASSED!")
print("=" * 60)
return 0
else:
print("=" * 60)
print("✗ SOME TESTS FAILED")
print("=" * 60)
return 1
if __name__ == "__main__":
sys.exit(main())