mirror of
https://github.com/JamesTheGiblet/BuddAI.git
synced 2026-01-08 21:58:40 +00:00
Add comprehensive test suite for BuddAI v3.1
- Implemented tests for database initialization, SQL injection prevention, auto-learning pattern extraction, module detection, complexity detection, LRU cache performance, session export, actionable suggestions, repository indexing, search query safety, and context window management. - Utilized SQLite for database operations and temporary directories for test isolation. - Included detailed output for test results with color-coded pass/fail indicators.
This commit is contained in:
parent
406d848203
commit
da3530774b
8 changed files with 1885 additions and 21 deletions
|
|
@ -361,7 +361,7 @@ class BuddAI:
|
|||
count = 0
|
||||
|
||||
for file_path in path.rglob('*'):
|
||||
if file_path.is_file() and file_path.suffix in ['.py', '.ino', '.cpp', '.h', '.js', '.jsx', '.html', '.css']:
|
||||
if file_path.is_file() and file_path.suffix in ['.py', '.ino', '.cpp', '.h']:
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
content = f.read()
|
||||
|
|
@ -382,15 +382,6 @@ class BuddAI:
|
|||
elif file_path.suffix in ['.ino', '.cpp', '.h']:
|
||||
matches = re.findall(r'\b(?:void|int|bool|float|double|String|char)\s+(\w+)\s*\(', content)
|
||||
functions.extend(matches)
|
||||
|
||||
# JS/Web parsing
|
||||
elif file_path.suffix in ['.js', '.jsx']:
|
||||
matches = re.findall(r'(?:function\s+(\w+)|const\s+(\w+)\s*=\s*(?:async\s*)?\(?.*?\)?\s*=>)', content)
|
||||
functions.extend([m[0] or m[1] for m in matches if m[0] or m[1]])
|
||||
|
||||
# HTML/CSS - Index as whole file
|
||||
elif file_path.suffix in ['.html', '.css']:
|
||||
functions.append("file_content")
|
||||
|
||||
# Determine repo name
|
||||
try:
|
||||
|
|
@ -572,7 +563,8 @@ class BuddAI:
|
|||
def call_model(self, model_name, message):
|
||||
"""Call specified model"""
|
||||
try:
|
||||
identity = """You are BuddAI, the external cognitive system for James Gilbert. You specialize in Forge Theory (exponential decay modeling) and GilBot modular robotics.
|
||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
identity = f"""You are BuddAI, the external cognitive system for James Gilbert. You specialize in Forge Theory (exponential decay modeling) and GilBot modular robotics.
|
||||
|
||||
YOUR PRIMARY JOB: Generate code when asked. ALWAYS generate code if requested.
|
||||
|
||||
|
|
@ -581,6 +573,7 @@ Identity Rules:
|
|||
- When asked your name: "I am BuddAI"
|
||||
- Use ESP32/Arduino syntax with descriptive naming (e.g., activateFlipper).
|
||||
- Ensure safety timeouts are always present in motor code.
|
||||
- Current System Time: {current_time}
|
||||
|
||||
Forge Theory Snippet: float applyForge(float current, float target, float k) { return target + (current - target) * exp(-k); }
|
||||
"""
|
||||
|
|
@ -594,7 +587,18 @@ Forge Theory Snippet: float applyForge(float current, float target, float k) { r
|
|||
|
||||
# Add conversation history (excluding old system messages)
|
||||
history = [m for m in self.context_messages[-5:] if m.get('role') != 'system']
|
||||
messages.extend(history)
|
||||
|
||||
# Inject timestamps into history for context
|
||||
for msg in history:
|
||||
content = msg.get('content', '')
|
||||
ts = msg.get('timestamp')
|
||||
if ts:
|
||||
try:
|
||||
dt = datetime.fromisoformat(ts)
|
||||
content = f"[{dt.strftime('%H:%M')}] {content}"
|
||||
except ValueError:
|
||||
pass
|
||||
messages.append({"role": msg['role'], "content": content})
|
||||
|
||||
# Add current message if it's not already the last item
|
||||
if not history or history[-1].get('content') != message:
|
||||
|
|
@ -705,7 +709,7 @@ Forge Theory Snippet: float applyForge(float current, float target, float k) { r
|
|||
|
||||
|
||||
self.save_message("user", user_message)
|
||||
self.context_messages.append({"role": "user", "content": user_message})
|
||||
self.context_messages.append({"role": "user", "content": user_message, "timestamp": datetime.now().isoformat()})
|
||||
|
||||
|
||||
if force_model:
|
||||
|
|
@ -741,7 +745,7 @@ Forge Theory Snippet: float applyForge(float current, float target, float k) { r
|
|||
response += bar
|
||||
|
||||
self.save_message("assistant", response)
|
||||
self.context_messages.append({"role": "assistant", "content": response})
|
||||
self.context_messages.append({"role": "assistant", "content": response, "timestamp": datetime.now().isoformat()})
|
||||
|
||||
return response
|
||||
|
||||
|
|
@ -855,7 +859,7 @@ if SERVER_AVAILABLE:
|
|||
return {"message": f"✅ Successfully indexed {file.filename}"}
|
||||
else:
|
||||
# Support single code files by moving them to a folder and indexing
|
||||
if file_location.suffix in ['.py', '.ino', '.cpp', '.h', '.js', '.jsx', '.html', '.css']:
|
||||
if file_location.suffix in ['.py', '.ino', '.cpp', '.h']:
|
||||
target_dir = uploads_dir / file_location.stem
|
||||
target_dir.mkdir(exist_ok=True)
|
||||
final_path = target_dir / file.filename
|
||||
1020
buddai_v3.1.py
Normal file
1020
buddai_v3.1.py
Normal file
File diff suppressed because it is too large
Load diff
78
examples/buddai_generated.cpp
Normal file
78
examples/buddai_generated.cpp
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* BuddAI Complex Test Sketch
|
||||
* Purpose: Verify .ino detection in frontend
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// Configuration
|
||||
const int LED_PIN = 2;
|
||||
const int SENSOR_PIN = 34;
|
||||
const unsigned long INTERVAL = 1000;
|
||||
|
||||
// State Machine
|
||||
enum State {
|
||||
IDLE,
|
||||
ACTIVE,
|
||||
ERROR
|
||||
};
|
||||
|
||||
State currentState = IDLE;
|
||||
unsigned long previousMillis = 0;
|
||||
|
||||
// Function Prototypes
|
||||
void updateState();
|
||||
void handleSensors();
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
pinMode(LED_PIN, OUTPUT);
|
||||
pinMode(SENSOR_PIN, INPUT);
|
||||
|
||||
Serial.println("BuddAI System Initialized");
|
||||
Serial.println("Waiting for input...");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
unsigned long currentMillis = millis();
|
||||
|
||||
// Non-blocking timer
|
||||
if (currentMillis - previousMillis >= INTERVAL) {
|
||||
previousMillis = currentMillis;
|
||||
updateState();
|
||||
}
|
||||
|
||||
handleSensors();
|
||||
}
|
||||
|
||||
void updateState() {
|
||||
switch (currentState) {
|
||||
case IDLE:
|
||||
digitalWrite(LED_PIN, LOW);
|
||||
Serial.println("State: IDLE");
|
||||
currentState = ACTIVE;
|
||||
break;
|
||||
|
||||
case ACTIVE:
|
||||
digitalWrite(LED_PIN, HIGH);
|
||||
Serial.println("State: ACTIVE");
|
||||
// Simulate work
|
||||
currentState = IDLE;
|
||||
break;
|
||||
|
||||
case ERROR:
|
||||
digitalWrite(LED_PIN, HIGH);
|
||||
delay(100);
|
||||
digitalWrite(LED_PIN, LOW);
|
||||
delay(100);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void handleSensors() {
|
||||
int reading = analogRead(SENSOR_PIN);
|
||||
if (reading > 4000) {
|
||||
currentState = ERROR;
|
||||
Serial.println("Alert: Sensor Overload!");
|
||||
}
|
||||
}
|
||||
21
examples/buddai_generated.csharp
Normal file
21
examples/buddai_generated.csharp
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Define the LED pin
|
||||
const int ledPin = 9;
|
||||
|
||||
void setup() {
|
||||
// Set up the LED pin as an output
|
||||
pinMode(ledPin, OUTPUT);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Turn the LED on
|
||||
digitalWrite(ledPin, HIGH);
|
||||
|
||||
// Wait for 1 second (1000 milliseconds)
|
||||
delay(1000);
|
||||
|
||||
// Turn the LED off
|
||||
digitalWrite(ledPin, LOW);
|
||||
|
||||
// Wait for another 1 second
|
||||
delay(1000);
|
||||
}
|
||||
1
examples/buddai_generated.typescript
Normal file
1
examples/buddai_generated.typescript
Normal file
|
|
@ -0,0 +1 @@
|
|||
[object Object]
|
||||
|
|
@ -3,8 +3,8 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔥</text></svg>">
|
||||
<title>🔥 BuddAI Web</title>
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<title>BuddAI Web</title>
|
||||
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
|
|
@ -42,10 +42,26 @@
|
|||
--code-border: #d1d5da;
|
||||
--code-text: #24292e;
|
||||
}
|
||||
.sidebar-left { width: 260px; background: var(--header-bg); border-right: 1px solid var(--border-color); display: flex; flex-direction: column; flex-shrink: 0; }
|
||||
.session-list { flex: 1; overflow-y: auto; }
|
||||
.session-item { padding: 12px 15px; cursor: pointer; border-bottom: 1px solid var(--border-color); font-size: 0.85em; color: var(--text-color); transition: background 0.2s; }
|
||||
.session-item:hover { background: var(--bg-color); }
|
||||
.session-item.active { background: var(--btn-bg); color: white; border-color: var(--btn-bg); }
|
||||
.new-chat-btn { margin: 15px; padding: 10px; background: var(--btn-bg); color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; }
|
||||
.new-chat-btn:hover { background: var(--btn-hover); }
|
||||
.session-date { font-weight: bold; display: block; margin-bottom: 4px; }
|
||||
.session-id { font-size: 0.8em; opacity: 0.6; font-family: monospace; }
|
||||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: var(--bg-color); color: var(--text-color); margin: 0; display: flex; justify-content: center; height: 100vh; transition: background 0.3s, color 0.3s; }
|
||||
#root { width: 100%; max-width: 900px; display: flex; flex-direction: column; height: 100%; }
|
||||
#root { width: 100%; max-width: 100%; display: flex; flex-direction: column; height: 100%; }
|
||||
.main-layout { display: flex; flex: 1; overflow: hidden; }
|
||||
.chat-section { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
||||
.chat-container { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 15px; }
|
||||
.side-panel { width: 45%; border-left: 1px solid var(--border-color); display: flex; flex-direction: column; background: var(--bg-color); }
|
||||
.side-header { padding: 10px 15px; background: var(--header-bg); border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; font-size: 0.9em; }
|
||||
.side-content { flex: 1; overflow: auto; background: var(--code-bg); position: relative; }
|
||||
.side-content pre { margin: 0; padding: 15px; min-height: 100%; box-sizing: border-box; }
|
||||
.message { padding: 15px; border-radius: 8px; max-width: 85%; line-height: 1.5; }
|
||||
.timestamp { font-size: 0.75em; opacity: 0.7; margin-bottom: 4px; display: block; font-family: monospace; }
|
||||
.user { align-self: flex-end; background: var(--user-msg-bg); color: var(--user-msg-text); }
|
||||
.assistant { align-self: flex-start; background: var(--assistant-msg-bg); border: 1px solid var(--border-color); }
|
||||
.input-area { padding: 20px; background: var(--header-bg); border-top: 1px solid var(--border-color); display: flex; gap: 10px; }
|
||||
|
|
@ -89,6 +105,12 @@
|
|||
// Configure Marked for Code Copy
|
||||
const renderer = new marked.Renderer();
|
||||
renderer.code = (code, language) => {
|
||||
// Handle Marked v12+ signature where first arg is a token object
|
||||
if (typeof code === 'object' && code !== null && code.text) {
|
||||
language = code.lang;
|
||||
code = code.text;
|
||||
}
|
||||
|
||||
const validLang = (language && hljs.getLanguage(language)) ? language : 'plaintext';
|
||||
let highlighted = code;
|
||||
try {
|
||||
|
|
@ -99,7 +121,10 @@
|
|||
return `<div class="code-wrapper">
|
||||
<div class="code-header">
|
||||
<span>${language || 'text'}</span>
|
||||
<button class="copy-code-btn" onclick="window.copyToClipboard(this)">Copy</button>
|
||||
<div style="display:flex; gap:5px;">
|
||||
<button class="copy-code-btn" onclick="window.copyToClipboard(this)">Copy</button>
|
||||
<button class="copy-code-btn" onclick="window.sendToSidebar(this)">Sidebar</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre><code class="hljs ${validLang}">${highlighted}</code></pre>
|
||||
</div>`;
|
||||
|
|
@ -115,16 +140,60 @@
|
|||
setTimeout(() => btn.innerText = original, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
window.sendToSidebar = (btn) => {
|
||||
const wrapper = btn.closest('.code-wrapper');
|
||||
const code = wrapper.querySelector('code').innerText;
|
||||
if (window.updateSidebar) window.updateSidebar(code);
|
||||
};
|
||||
|
||||
window.downloadCode = (content) => {
|
||||
if (!content) return;
|
||||
if (typeof content !== 'string') return;
|
||||
let ext = 'txt';
|
||||
|
||||
// Heuristic for Arduino
|
||||
if (content.includes('void setup()') && content.includes('void loop()')) {
|
||||
ext = 'ino';
|
||||
} else {
|
||||
try {
|
||||
const result = hljs.highlightAuto(content);
|
||||
if (result.language) {
|
||||
const map = {
|
||||
python: 'py', javascript: 'js', cpp: 'cpp', c: 'c',
|
||||
arduino: 'ino', html: 'html', css: 'css',
|
||||
csharp: 'cs', java: 'java', bash: 'sh', json: 'json',
|
||||
markdown: 'md', sql: 'sql'
|
||||
};
|
||||
ext = map[result.language] || result.language;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `buddai_generated.${ext}`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const { useState, useEffect, useRef } = React;
|
||||
|
||||
function App() {
|
||||
const [history, setHistory] = useState([]);
|
||||
const [sessions, setSessions] = useState([]);
|
||||
const [currentSessionId, setCurrentSessionId] = useState(null);
|
||||
const [input, setInput] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [status, setStatus] = useState("connecting");
|
||||
const [forgeMode, setForgeMode] = useState("2");
|
||||
const [theme, setTheme] = useState("dark");
|
||||
const [sidebarContent, setSidebarContent] = useState("");
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
const endRef = useRef(null);
|
||||
const abortControllerRef = useRef(null);
|
||||
|
||||
|
|
@ -133,6 +202,11 @@
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.updateSidebar = (code) => {
|
||||
setSidebarContent(code);
|
||||
setIsSidebarOpen(true);
|
||||
};
|
||||
|
||||
document.body.className = theme === 'light' ? 'light-mode' : '';
|
||||
const hljsTheme = document.getElementById('hljs-theme');
|
||||
if (hljsTheme) {
|
||||
|
|
@ -146,6 +220,8 @@
|
|||
scrollToBottom();
|
||||
}, [history]);
|
||||
|
||||
const fetchSessions = () => fetch("/api/sessions").then(res => res.json()).then(data => setSessions(data.sessions || [])).catch(console.error);
|
||||
|
||||
useEffect(() => {
|
||||
// Check System Status
|
||||
const checkStatus = async () => {
|
||||
|
|
@ -159,6 +235,7 @@
|
|||
checkStatus();
|
||||
const timer = setInterval(checkStatus, 10000);
|
||||
|
||||
fetchSessions();
|
||||
// Load History
|
||||
fetch("/api/history")
|
||||
.then(res => res.json())
|
||||
|
|
@ -171,11 +248,34 @@
|
|||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
const loadSession = async (sessionId) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/session/load", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ session_id: sessionId })
|
||||
});
|
||||
const data = await res.json();
|
||||
setHistory(data.history || []);
|
||||
setCurrentSessionId(data.session_id);
|
||||
} catch (e) { console.error(e); }
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const startNewSession = async () => {
|
||||
const res = await fetch("/api/session/new", { method: "POST" });
|
||||
const data = await res.json();
|
||||
setCurrentSessionId(data.session_id);
|
||||
setHistory([]);
|
||||
fetchSessions();
|
||||
};
|
||||
|
||||
const sendMessage = async (textOverride = null) => {
|
||||
const msgText = typeof textOverride === 'string' ? textOverride : input;
|
||||
if (!msgText.trim()) return;
|
||||
|
||||
const userMsg = { role: "user", content: msgText };
|
||||
const userMsg = { role: "user", content: msgText, timestamp: new Date().toISOString() };
|
||||
setHistory(prev => [...prev, userMsg]);
|
||||
if (!textOverride) setInput("");
|
||||
setLoading(true);
|
||||
|
|
@ -194,6 +294,7 @@
|
|||
});
|
||||
const data = await res.json();
|
||||
setHistory(prev => [...prev, { role: "assistant", content: data.response }]);
|
||||
if (!currentSessionId) fetchSessions(); // Refresh list if this was first msg
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') {
|
||||
setHistory(prev => [...prev, { role: "assistant", content: "🛑 *Generation stopped by user.*" }]);
|
||||
|
|
@ -245,7 +346,8 @@
|
|||
<>
|
||||
<div className="header">
|
||||
<div style={{display:'flex', alignItems:'center'}}>
|
||||
<h3 style={{margin:0}}>🔥 BuddAI v3.0</h3>
|
||||
<img src="/favicon.ico" alt="BuddAI" style={{height: '24px', marginRight: '10px'}} />
|
||||
<h3 style={{margin:0}}>BuddAI v3</h3>
|
||||
<span className={`status-badge ${status}`}>{status}</span>
|
||||
</div>
|
||||
<div style={{display:'flex', gap:'10px'}}>
|
||||
|
|
@ -256,6 +358,7 @@
|
|||
onChange={handleFileUpload}
|
||||
/>
|
||||
<button className="clear-btn" onClick={() => document.getElementById('upload-input').click()}>📂 Upload</button>
|
||||
<button className="clear-btn" onClick={() => setIsSidebarOpen(!isSidebarOpen)}>{isSidebarOpen ? 'Hide Code' : 'Show Code'}</button>
|
||||
<button className="clear-btn" onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>{theme === 'dark' ? '☀️' : '🌙'}</button>
|
||||
<select
|
||||
value={forgeMode}
|
||||
|
|
@ -268,12 +371,27 @@
|
|||
<button className="clear-btn" onClick={() => setHistory([])}>Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="main-layout">
|
||||
<div className="sidebar-left">
|
||||
<button className="new-chat-btn" onClick={startNewSession}>+ New Chat</button>
|
||||
<div className="session-list">
|
||||
{sessions.map(s => (
|
||||
<div key={s.id} className={`session-item ${s.id === currentSessionId ? 'active' : ''}`} onClick={() => loadSession(s.id)}>
|
||||
<span className="session-date">{new Date(s.date).toLocaleDateString()} {new Date(s.date).toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}</span>
|
||||
<span className="session-id">{s.id}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="chat-section">
|
||||
<div className="chat-container">
|
||||
{history.length === 0 && <div style={{textAlign: 'center', marginTop: '50px', color: '#666'}}><p>Ready to build.</p></div>}
|
||||
{history.map((msg, i) => {
|
||||
const { text, suggestions } = msg.role === 'assistant' ? parseContent(msg.content) : { text: msg.content, suggestions: [] };
|
||||
const timeStr = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) : '';
|
||||
return (
|
||||
<div key={i} className={`message ${msg.role}`}>
|
||||
{timeStr && <span className="timestamp">{timeStr}</span>}
|
||||
<div dangerouslySetInnerHTML={{ __html: marked.parse(text) }} />
|
||||
{suggestions.length > 0 && (
|
||||
<div className="suggestions">
|
||||
|
|
@ -302,6 +420,23 @@
|
|||
<button onClick={() => sendMessage()}>Send</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isSidebarOpen && (
|
||||
<div className="side-panel">
|
||||
<div className="side-header">
|
||||
<span>Code Workspace</span>
|
||||
<div style={{display: 'flex', gap: '5px'}}>
|
||||
<button className="copy-code-btn" onClick={() => setSidebarContent("")}>Clear</button>
|
||||
<button className="copy-code-btn" onClick={() => navigator.clipboard.writeText(sidebarContent)}>Copy</button>
|
||||
<button className="copy-code-btn" onClick={() => window.downloadCode(sidebarContent)}>Download</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="side-content">
|
||||
<pre><code className="hljs" dangerouslySetInnerHTML={{__html: sidebarContent ? hljs.highlightAuto(sidebarContent).value : '// Select code from chat to view here'}} /></pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
BIN
icons/icon.png
Normal file
BIN
icons/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
605
tests/test_buddai.py
Normal file
605
tests/test_buddai.py
Normal file
|
|
@ -0,0 +1,605 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
BuddAI v3.1 Test Suite
|
||||
Comprehensive testing for all features
|
||||
|
||||
Author: James Gilbert
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
import sys
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# Test utilities
|
||||
class TestColors:
|
||||
PASS = '\033[92m'
|
||||
FAIL = '\033[91m'
|
||||
INFO = '\033[94m'
|
||||
WARN = '\033[93m'
|
||||
END = '\033[0m'
|
||||
|
||||
def print_test(name):
|
||||
print(f"\n{TestColors.INFO}🧪 Testing: {name}{TestColors.END}")
|
||||
|
||||
def print_pass(message):
|
||||
print(f" {TestColors.PASS}✅ {message}{TestColors.END}")
|
||||
|
||||
def print_fail(message):
|
||||
print(f" {TestColors.FAIL}❌ {message}{TestColors.END}")
|
||||
|
||||
def print_warn(message):
|
||||
print(f" {TestColors.WARN}⚠️ {message}{TestColors.END}")
|
||||
|
||||
|
||||
# Test 1: Database Initialization
|
||||
def test_database_init():
|
||||
print_test("Database Initialization")
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
|
||||
# Create tables
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
started_at TIMESTAMP,
|
||||
ended_at TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT,
|
||||
role TEXT,
|
||||
content TEXT,
|
||||
timestamp TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS repo_index (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
file_path TEXT,
|
||||
repo_name TEXT,
|
||||
function_name TEXT,
|
||||
content TEXT,
|
||||
last_modified TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS style_preferences (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
category TEXT,
|
||||
preference TEXT,
|
||||
confidence FLOAT,
|
||||
extracted_at TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Verify tables exist
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
tables = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
required_tables = ['sessions', 'messages', 'repo_index', 'style_preferences']
|
||||
all_exist = all(table in tables for table in required_tables)
|
||||
|
||||
conn.close()
|
||||
|
||||
if all_exist:
|
||||
print_pass(f"All {len(required_tables)} tables created successfully")
|
||||
return True
|
||||
else:
|
||||
missing = [t for t in required_tables if t not in tables]
|
||||
print_fail(f"Missing tables: {', '.join(missing)}")
|
||||
return False
|
||||
|
||||
|
||||
# Test 2: SQL Injection Prevention
|
||||
def test_sql_injection_prevention():
|
||||
print_test("SQL Injection Prevention")
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE repo_index (
|
||||
id INTEGER PRIMARY KEY,
|
||||
function_name TEXT,
|
||||
content TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
# Insert test data
|
||||
cursor.execute("INSERT INTO repo_index (function_name, content) VALUES (?, ?)",
|
||||
("testFunc", "test content"))
|
||||
conn.commit()
|
||||
|
||||
# Test malicious input
|
||||
malicious_input = "'; DROP TABLE repo_index; --"
|
||||
|
||||
# SECURE: Parameterized query
|
||||
try:
|
||||
cursor.execute("SELECT * FROM repo_index WHERE function_name LIKE ?",
|
||||
(f"%{malicious_input}%",))
|
||||
results = cursor.fetchall()
|
||||
|
||||
# Verify table still exists
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='repo_index'")
|
||||
table_exists = cursor.fetchone() is not None
|
||||
|
||||
conn.close()
|
||||
|
||||
if table_exists:
|
||||
print_pass("Parameterized queries prevent SQL injection")
|
||||
print_pass("Table survived malicious input")
|
||||
return True
|
||||
else:
|
||||
print_fail("Table was dropped - vulnerable to injection!")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print_fail(f"Query failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# Test 3: Auto-Learning Pattern Extraction
|
||||
def test_auto_learning():
|
||||
print_test("Auto-Learning Pattern Extraction")
|
||||
|
||||
sample_code = """
|
||||
#include <Arduino.h>
|
||||
|
||||
#define MOTOR_PIN 5
|
||||
const int TIMEOUT_MS = 5000;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
ledcSetup(0, 500, 8);
|
||||
}
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
patterns = {
|
||||
'serial_baud': re.search(r'Serial\.begin\((\d+)\)', sample_code),
|
||||
'pin_style': 'define' if '#define' in sample_code else 'const',
|
||||
'timeout_value': re.search(r'TIMEOUT.*?(\d+)', sample_code),
|
||||
'pwm_freq': re.search(r'ledcSetup\([^,]+,\s*(\d+)', sample_code),
|
||||
}
|
||||
|
||||
extracted = {}
|
||||
for key, value in patterns.items():
|
||||
if value:
|
||||
extracted[key] = value.group(1) if hasattr(value, 'group') else str(value)
|
||||
|
||||
expected = {
|
||||
'serial_baud': '115200',
|
||||
'pin_style': 'define',
|
||||
'timeout_value': '5000',
|
||||
'pwm_freq': '500'
|
||||
}
|
||||
|
||||
success = True
|
||||
for key, expected_val in expected.items():
|
||||
actual_val = extracted.get(key)
|
||||
if actual_val == expected_val:
|
||||
print_pass(f"Extracted {key}: {actual_val}")
|
||||
else:
|
||||
print_fail(f"Failed to extract {key} (got {actual_val}, expected {expected_val})")
|
||||
success = False
|
||||
|
||||
return success
|
||||
|
||||
|
||||
# Test 4: Module Detection
|
||||
def test_module_detection():
|
||||
print_test("Module Detection")
|
||||
|
||||
MODULE_PATTERNS = {
|
||||
"ble": ["bluetooth", "ble", "wireless"],
|
||||
"servo": ["servo", "flipper", "weapon"],
|
||||
"motor": ["motor", "drive", "movement", "l298n"],
|
||||
"safety": ["safety", "timeout", "failsafe"],
|
||||
}
|
||||
|
||||
test_cases = [
|
||||
("Generate code with BLE and servo control", ["ble", "servo"]),
|
||||
("Add motor driver with safety timeout", ["motor", "safety"]),
|
||||
("Build complete robot with bluetooth, motors, and weapon", ["ble", "motor", "servo"]),
|
||||
]
|
||||
|
||||
def extract_modules(message):
|
||||
message_lower = message.lower()
|
||||
detected = []
|
||||
for module, keywords in MODULE_PATTERNS.items():
|
||||
if any(kw in message_lower for kw in keywords):
|
||||
detected.append(module)
|
||||
return detected
|
||||
|
||||
success = True
|
||||
for message, expected_modules in test_cases:
|
||||
detected = extract_modules(message)
|
||||
if set(detected) == set(expected_modules):
|
||||
print_pass(f"Detected: {detected} from '{message[:50]}...'")
|
||||
else:
|
||||
print_fail(f"Expected {expected_modules}, got {detected}")
|
||||
success = False
|
||||
|
||||
return success
|
||||
|
||||
|
||||
# Test 5: Complexity Detection
|
||||
def test_complexity_detection():
|
||||
print_test("Complexity Detection")
|
||||
|
||||
COMPLEX_TRIGGERS = ["complete", "entire", "full", "build entire"]
|
||||
MODULE_PATTERNS = {
|
||||
"ble": ["bluetooth", "ble"],
|
||||
"servo": ["servo"],
|
||||
"motor": ["motor"],
|
||||
}
|
||||
|
||||
def is_complex(message):
|
||||
message_lower = message.lower()
|
||||
trigger_count = sum(1 for trigger in COMPLEX_TRIGGERS if trigger in message_lower)
|
||||
module_count = sum(1 for module, keywords in MODULE_PATTERNS.items()
|
||||
if any(kw in message_lower for kw in keywords))
|
||||
return trigger_count >= 2 or module_count >= 3
|
||||
|
||||
test_cases = [
|
||||
("Generate a motor driver class", False),
|
||||
("Build complete robot with BLE, servo, and motors", True),
|
||||
("Create entire system with full integration", True),
|
||||
("What pins should I use?", False),
|
||||
]
|
||||
|
||||
success = True
|
||||
for message, expected_complex in test_cases:
|
||||
detected = is_complex(message)
|
||||
if detected == expected_complex:
|
||||
complexity = "COMPLEX" if detected else "SIMPLE"
|
||||
print_pass(f"{complexity}: '{message}'")
|
||||
else:
|
||||
print_fail(f"Expected {expected_complex}, got {detected} for '{message}'")
|
||||
success = False
|
||||
|
||||
return success
|
||||
|
||||
|
||||
# Test 6: LRU Cache Performance
|
||||
def test_lru_cache():
|
||||
print_test("LRU Cache Performance")
|
||||
|
||||
from functools import lru_cache
|
||||
import time
|
||||
|
||||
call_count = 0
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def cached_function(keywords):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
time.sleep(0.01) # Simulate slow operation
|
||||
return f"Result for {keywords}"
|
||||
|
||||
# First call - should execute
|
||||
cached_function(("motor", "servo"))
|
||||
first_count = call_count
|
||||
|
||||
# Second call - should use cache
|
||||
cached_function(("motor", "servo"))
|
||||
second_count = call_count
|
||||
|
||||
# Different call - should execute
|
||||
cached_function(("ble", "battery"))
|
||||
third_count = call_count
|
||||
|
||||
if first_count == 1 and second_count == 1 and third_count == 2:
|
||||
print_pass("Cache working: 2nd call skipped execution")
|
||||
print_pass(f"Function called {call_count} times for 3 queries")
|
||||
return True
|
||||
else:
|
||||
print_fail(f"Cache not working properly: {first_count}, {second_count}, {third_count}")
|
||||
return False
|
||||
|
||||
|
||||
# Test 7: Session Export
|
||||
def test_session_export():
|
||||
print_test("Session Export")
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
export_path = Path(tmpdir) / "test_export.md"
|
||||
|
||||
# Simulate export
|
||||
session_id = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
messages = [
|
||||
("user", "Generate motor code", "2025-12-28 10:00:00"),
|
||||
("assistant", "```cpp\nvoid setupMotors() {}\n```", "2025-12-28 10:00:05"),
|
||||
]
|
||||
|
||||
output = f"# BuddAI Session Export\n"
|
||||
output += f"**Session ID:** {session_id}\n\n"
|
||||
output += "---\n\n"
|
||||
|
||||
for role, content, timestamp in messages:
|
||||
if role == 'user':
|
||||
output += f"## 🧑 James ({timestamp})\n{content}\n\n"
|
||||
else:
|
||||
output += f"## 🤖 BuddAI\n{content}\n\n"
|
||||
|
||||
with open(export_path, 'w', encoding='utf-8') as f:
|
||||
f.write(output)
|
||||
|
||||
# Verify export
|
||||
if export_path.exists():
|
||||
content = export_path.read_text(encoding='utf-8')
|
||||
has_session_id = session_id in content
|
||||
has_code = "```cpp" in content
|
||||
has_headers = "## " in content and "James" in content # More flexible check
|
||||
|
||||
if has_session_id and has_code and has_headers:
|
||||
print_pass("Export file created with correct format")
|
||||
print_pass(f"File size: {len(content)} bytes")
|
||||
return True
|
||||
else:
|
||||
if not has_session_id:
|
||||
print_fail("Missing session ID")
|
||||
if not has_code:
|
||||
print_fail("Missing code blocks")
|
||||
if not has_headers:
|
||||
print_fail("Missing headers")
|
||||
return False
|
||||
else:
|
||||
print_fail("Export file not created")
|
||||
return False
|
||||
|
||||
|
||||
# Test 8: Actionable Suggestions
|
||||
def test_actionable_suggestions():
|
||||
print_test("Actionable Suggestions")
|
||||
|
||||
user_input = "Generate motor driver with L298N"
|
||||
generated_code = """
|
||||
void setupMotors() {
|
||||
pinMode(MOTOR_PIN, OUTPUT);
|
||||
}
|
||||
"""
|
||||
|
||||
suggestions = []
|
||||
|
||||
# Forge Theory Check
|
||||
if ("motor" in user_input.lower() or "servo" in user_input.lower()) and "applyForge" not in generated_code:
|
||||
suggestions.append({
|
||||
'text': "Apply Forge Theory smoothing?",
|
||||
'action': 'add_forge',
|
||||
'code': "float applyForge(float current, float target, float k) { return target + (current - target) * exp(-k); }"
|
||||
})
|
||||
|
||||
# Safety Check
|
||||
if "L298N" in user_input and "safety" not in generated_code.lower():
|
||||
suggestions.append({
|
||||
'text': "Add 5s safety timeout?",
|
||||
'action': 'add_safety',
|
||||
'code': "unsigned long lastCommandTime = 0;\nconst unsigned long TIMEOUT_MS = 5000;"
|
||||
})
|
||||
|
||||
if len(suggestions) == 2:
|
||||
print_pass(f"Generated {len(suggestions)} actionable suggestions")
|
||||
for i, s in enumerate(suggestions, 1):
|
||||
print_pass(f" {i}. {s['text']} (action: {s['action']})")
|
||||
if s['code']:
|
||||
print_pass(f" Code snippet: {len(s['code'])} chars")
|
||||
return True
|
||||
else:
|
||||
print_fail(f"Expected 2 suggestions, got {len(suggestions)}")
|
||||
return False
|
||||
|
||||
|
||||
# Test 9: Repository Indexing
|
||||
def test_repository_indexing():
|
||||
print_test("Repository Indexing")
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# Create test repository structure
|
||||
repo_dir = Path(tmpdir) / "test_repo"
|
||||
repo_dir.mkdir()
|
||||
|
||||
# Create test files
|
||||
test_files = {
|
||||
"motor_driver.ino": """
|
||||
void setupMotors() {
|
||||
Serial.begin(115200);
|
||||
pinMode(MOTOR_PIN, OUTPUT);
|
||||
}
|
||||
|
||||
void driveForward(int speed) {
|
||||
digitalWrite(MOTOR_PIN, HIGH);
|
||||
}
|
||||
""",
|
||||
"servo_control.cpp": """
|
||||
#include <Servo.h>
|
||||
|
||||
void activateFlipper() {
|
||||
servo.write(90);
|
||||
}
|
||||
""",
|
||||
"utils.py": """
|
||||
def calculate_pwm(speed):
|
||||
return int(speed * 255 / 100)
|
||||
|
||||
def apply_forge(current, target, k):
|
||||
return target + (current - target) * math.exp(-k)
|
||||
"""
|
||||
}
|
||||
|
||||
for filename, content in test_files.items():
|
||||
(repo_dir / filename).write_text(content)
|
||||
|
||||
# Simulate indexing
|
||||
import re
|
||||
indexed_functions = []
|
||||
|
||||
for file_path in repo_dir.rglob('*'):
|
||||
if file_path.is_file() and file_path.suffix in ['.ino', '.cpp', '.py']:
|
||||
content = file_path.read_text()
|
||||
|
||||
if file_path.suffix in ['.ino', '.cpp']:
|
||||
matches = re.findall(r'\b(?:void|int)\s+(\w+)\s*\(', content)
|
||||
indexed_functions.extend(matches)
|
||||
elif file_path.suffix == '.py':
|
||||
matches = re.findall(r'\bdef\s+(\w+)\s*\(', content)
|
||||
indexed_functions.extend(matches)
|
||||
|
||||
expected_functions = ['setupMotors', 'driveForward', 'activateFlipper', 'calculate_pwm', 'apply_forge']
|
||||
|
||||
if set(indexed_functions) == set(expected_functions):
|
||||
print_pass(f"Indexed {len(indexed_functions)} functions correctly")
|
||||
for func in indexed_functions:
|
||||
print_pass(f" - {func}()")
|
||||
return True
|
||||
else:
|
||||
missing = set(expected_functions) - set(indexed_functions)
|
||||
extra = set(indexed_functions) - set(expected_functions)
|
||||
if missing:
|
||||
print_fail(f"Missing functions: {missing}")
|
||||
if extra:
|
||||
print_warn(f"Extra functions: {extra}")
|
||||
return False
|
||||
|
||||
|
||||
# Test 10: Search Query Safety
|
||||
def test_search_query_safety():
|
||||
print_test("Search Query Safety")
|
||||
|
||||
malicious_queries = [
|
||||
"'; DROP TABLE repo_index; --",
|
||||
"' OR '1'='1",
|
||||
"admin'--",
|
||||
"<script>alert('xss')</script>",
|
||||
]
|
||||
|
||||
import re
|
||||
|
||||
success = True
|
||||
for query in malicious_queries:
|
||||
# Extract keywords safely
|
||||
keywords = re.findall(r'\b\w{4,}\b', query.lower())
|
||||
|
||||
# Build parameterized query
|
||||
conditions = []
|
||||
params = []
|
||||
for keyword in keywords:
|
||||
conditions.append("(function_name LIKE ? OR content LIKE ?)")
|
||||
params.extend([f"%{keyword}%", f"%{keyword}%"])
|
||||
|
||||
# Verify no SQL injection possible
|
||||
if conditions:
|
||||
safe_sql = f"SELECT * FROM repo_index WHERE {' OR '.join(conditions)}"
|
||||
# SQL should only contain placeholders
|
||||
if "DROP" not in safe_sql and "'; " not in safe_sql:
|
||||
print_pass(f"Safely handled: '{query[:30]}...'")
|
||||
else:
|
||||
print_fail(f"Potential injection: '{query}'")
|
||||
success = False
|
||||
else:
|
||||
print_pass(f"Rejected empty query: '{query}'")
|
||||
|
||||
return success
|
||||
|
||||
|
||||
# Test 11: Context Window Management
|
||||
def test_context_window():
|
||||
print_test("Context Window Management")
|
||||
|
||||
context_messages = []
|
||||
|
||||
# Add many messages
|
||||
for i in range(20):
|
||||
context_messages.append({"role": "user", "content": f"Message {i}"})
|
||||
context_messages.append({"role": "assistant", "content": f"Response {i}"})
|
||||
|
||||
# Simulate limiting to last 5 messages
|
||||
limited_context = context_messages[-5:]
|
||||
|
||||
if len(limited_context) == 5:
|
||||
print_pass(f"Context limited to {len(limited_context)} messages (from {len(context_messages)})")
|
||||
print_pass(f"Oldest kept: '{limited_context[0]['content']}'")
|
||||
print_pass(f"Newest kept: '{limited_context[-1]['content']}'")
|
||||
return True
|
||||
else:
|
||||
print_fail(f"Context not limited properly: {len(limited_context)} messages")
|
||||
return False
|
||||
|
||||
|
||||
# Main Test Runner
|
||||
def run_all_tests():
|
||||
print("\n" + "="*60)
|
||||
print("🔥 BuddAI v3.1 Comprehensive Test Suite")
|
||||
print("="*60)
|
||||
|
||||
tests = [
|
||||
("Database Initialization", test_database_init),
|
||||
("SQL Injection Prevention", test_sql_injection_prevention),
|
||||
("Auto-Learning", test_auto_learning),
|
||||
("Module Detection", test_module_detection),
|
||||
("Complexity Detection", test_complexity_detection),
|
||||
("LRU Cache", test_lru_cache),
|
||||
("Session Export", test_session_export),
|
||||
("Actionable Suggestions", test_actionable_suggestions),
|
||||
("Repository Indexing", test_repository_indexing),
|
||||
("Search Query Safety", test_search_query_safety),
|
||||
("Context Window", test_context_window),
|
||||
]
|
||||
|
||||
results = []
|
||||
for name, test_func in tests:
|
||||
try:
|
||||
result = test_func()
|
||||
results.append((name, result))
|
||||
except Exception as e:
|
||||
print_fail(f"Test crashed: {e}")
|
||||
results.append((name, False))
|
||||
|
||||
# Summary
|
||||
print("\n" + "="*60)
|
||||
print("📊 Test Results Summary")
|
||||
print("="*60)
|
||||
|
||||
passed = sum(1 for _, result in results if result)
|
||||
total = len(results)
|
||||
|
||||
for name, result in results:
|
||||
status = f"{TestColors.PASS}✅ PASS{TestColors.END}" if result else f"{TestColors.FAIL}❌ FAIL{TestColors.END}"
|
||||
print(f"{status} - {name}")
|
||||
|
||||
print("\n" + "="*60)
|
||||
percentage = int((passed / total) * 100)
|
||||
|
||||
if passed == total:
|
||||
print(f"{TestColors.PASS}🎉 ALL TESTS PASSED: {passed}/{total} ({percentage}%){TestColors.END}")
|
||||
elif passed >= total * 0.8:
|
||||
print(f"{TestColors.WARN}⚠️ MOST TESTS PASSED: {passed}/{total} ({percentage}%){TestColors.END}")
|
||||
else:
|
||||
print(f"{TestColors.FAIL}❌ TESTS FAILED: {passed}/{total} ({percentage}%){TestColors.END}")
|
||||
|
||||
print("="*60 + "\n")
|
||||
|
||||
return passed == total
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = run_all_tests()
|
||||
sys.exit(0 if success else 1)
|
||||
Loading…
Add table
Add a link
Reference in a new issue