feat: json format, et al.

This commit is contained in:
Subarashimo
2025-12-03 14:55:30 +01:00
parent 56349f30e6
commit 0f7fdfcef1
28 changed files with 5692 additions and 237 deletions
+355
View File
@@ -0,0 +1,355 @@
/**
* RPG Companion Integration Test Helper
*
* This module provides functions for testing JSON parsing and validation
* within SillyTavern. It can be loaded in the browser console or integrated
* into the extension's debug mode.
*
* Usage in browser console:
* 1. Open SillyTavern with RPG Companion enabled
* 2. Open browser dev tools (F12)
* 3. Copy/paste this file's contents into console
* 4. Run: RPGTestHelper.validateLastResponse() or other methods
*/
window.RPGTestHelper = {
/**
* Validates the last generated tracker data against expected JSON structure
*/
validateLastResponse() {
console.log('🔍 Validating last generated tracker data...\n');
// Access extension settings (this assumes RPG Companion is loaded)
const settings = window.extension_settings?.['rpg-companion'];
if (!settings) {
console.error('❌ RPG Companion not found in extension_settings');
return false;
}
const results = {
inventoryV3: this.validateInventory(settings.inventoryV3),
skillsV2: this.validateSkills(settings.skillsV2),
questsV2: this.validateQuests(settings.questsV2),
infoBoxData: this.validateInfoBox(settings.infoBoxData),
charactersData: this.validateCharacters(settings.charactersData)
};
console.log('\n📊 Validation Results:');
Object.entries(results).forEach(([key, valid]) => {
console.log(` ${valid ? '✅' : '❌'} ${key}`);
});
return Object.values(results).every(v => v);
},
/**
* Validates inventory structure
*/
validateInventory(inv) {
console.log('\n📦 Validating Inventory...');
if (!inv) {
console.log(' ⚠️ inventoryV3 is null/undefined');
return true; // Not an error if not populated yet
}
let valid = true;
// Check onPerson array
if (inv.onPerson && !Array.isArray(inv.onPerson)) {
console.log(' ❌ onPerson should be an array');
valid = false;
} else if (inv.onPerson?.length > 0) {
const item = inv.onPerson[0];
if (typeof item !== 'object' || !item.name) {
console.log(' ❌ onPerson items should be objects with name property');
valid = false;
} else {
console.log(` ✅ onPerson: ${inv.onPerson.length} items (e.g., "${item.name}")`);
}
}
// Check stored object
if (inv.stored && typeof inv.stored !== 'object') {
console.log(' ❌ stored should be an object');
valid = false;
} else if (inv.stored) {
const locations = Object.keys(inv.stored);
console.log(` ✅ stored: ${locations.length} locations`);
}
// Check assets array
if (inv.assets && !Array.isArray(inv.assets)) {
console.log(' ❌ assets should be an array');
valid = false;
} else if (inv.assets?.length > 0) {
console.log(` ✅ assets: ${inv.assets.length} items`);
}
// Check simplified array
if (inv.simplified && !Array.isArray(inv.simplified)) {
console.log(' ❌ simplified should be an array');
valid = false;
} else if (inv.simplified?.length > 0) {
console.log(` ✅ simplified: ${inv.simplified.length} items`);
}
return valid;
},
/**
* Validates skills structure
*/
validateSkills(skills) {
console.log('\n⚔️ Validating Skills...');
if (!skills) {
console.log(' ⚠️ skillsV2 is null/undefined');
return true;
}
if (typeof skills !== 'object') {
console.log(' ❌ skillsV2 should be an object');
return false;
}
let valid = true;
for (const [category, abilities] of Object.entries(skills)) {
if (!Array.isArray(abilities)) {
console.log(`${category} should be an array`);
valid = false;
continue;
}
abilities.forEach((ability, i) => {
if (typeof ability !== 'object' || !ability.name) {
console.log(`${category}[${i}] should be an object with name`);
valid = false;
}
});
console.log(`${category}: ${abilities.length} abilities`);
}
return valid;
},
/**
* Validates quests structure
*/
validateQuests(quests) {
console.log('\n📜 Validating Quests...');
if (!quests) {
console.log(' ⚠️ questsV2 is null/undefined');
return true;
}
let valid = true;
if (quests.main !== null && quests.main !== undefined) {
if (typeof quests.main === 'string') {
console.log(` ✅ main: "${quests.main}"`);
} else if (typeof quests.main === 'object' && quests.main.name) {
console.log(` ✅ main: "${quests.main.name}" (structured)`);
} else {
console.log(' ❌ main should be string or {name, description}');
valid = false;
}
}
if (quests.optional) {
if (!Array.isArray(quests.optional)) {
console.log(' ❌ optional should be an array');
valid = false;
} else {
console.log(` ✅ optional: ${quests.optional.length} quests`);
}
}
return valid;
},
/**
* Validates info box structure
*/
validateInfoBox(info) {
console.log('\n📍 Validating Info Box...');
if (!info) {
console.log(' ⚠️ infoBoxData is null/undefined');
return true;
}
const fields = ['date', 'weather', 'temperature', 'time', 'location'];
let valid = true;
fields.forEach(field => {
if (info[field] !== undefined && info[field] !== null) {
if (typeof info[field] !== 'string') {
console.log(`${field} should be a string`);
valid = false;
} else {
console.log(`${field}: "${info[field]}"`);
}
}
});
if (info.recentEvents) {
if (!Array.isArray(info.recentEvents)) {
console.log(' ❌ recentEvents should be an array');
valid = false;
} else {
console.log(` ✅ recentEvents: ${info.recentEvents.length} events`);
}
}
return valid;
},
/**
* Validates characters structure
*/
validateCharacters(chars) {
console.log('\n👥 Validating Characters...');
if (!chars) {
console.log(' ⚠️ charactersData is null/undefined');
return true;
}
if (!Array.isArray(chars)) {
console.log(' ❌ charactersData should be an array');
return false;
}
let valid = true;
chars.forEach((char, i) => {
if (typeof char !== 'object' || !char.name) {
console.log(` ❌ character[${i}] should have name`);
valid = false;
} else {
console.log(`${char.name}: ${char.relationship || 'no relationship'}`);
}
});
return valid;
},
/**
* Tests JSON extraction from a raw response string
*/
testJSONExtraction(responseText) {
console.log('\n🔬 Testing JSON Extraction...\n');
const jsonRegex = /```(?:json)?\s*([\s\S]*?)```/i;
const match = responseText.match(jsonRegex);
if (!match) {
console.log('❌ No JSON code block found');
return null;
}
console.log('✅ Found JSON code block');
try {
const parsed = JSON.parse(match[1].trim());
console.log('✅ JSON parsed successfully');
console.log('📋 Structure:', Object.keys(parsed).join(', '));
return parsed;
} catch (e) {
console.log('❌ JSON parse failed:', e.message);
// Try to fix common issues
console.log('🔧 Attempting to fix JSON...');
const fixed = match[1].trim()
.replace(/,\s*}/g, '}')
.replace(/,\s*]/g, ']');
try {
const fixedParsed = JSON.parse(fixed);
console.log('✅ Fixed JSON parsed successfully');
return fixedParsed;
} catch (e2) {
console.log('❌ Could not fix JSON:', e2.message);
return null;
}
}
},
/**
* Simulates a full parse cycle with a sample response
*/
simulateParseResponse(sampleResponse) {
console.log('\n🔄 Simulating Parse Response...\n');
const parsed = this.testJSONExtraction(sampleResponse);
if (parsed) {
console.log('\n📊 Validating parsed structure:');
if (parsed.userStats) {
console.log(' ✅ userStats present');
}
if (parsed.skills) {
console.log(' ✅ skills present');
}
if (parsed.inventory) {
console.log(' ✅ inventory present');
}
if (parsed.quests) {
console.log(' ✅ quests present');
}
if (parsed.infoBox) {
console.log(' ✅ infoBox present');
}
if (parsed.presentCharacters) {
console.log(' ✅ presentCharacters present');
}
}
return parsed;
},
/**
* Prints current extension settings for debugging
*/
printCurrentState() {
const settings = window.extension_settings?.['rpg-companion'];
if (!settings) {
console.error('RPG Companion not found');
return;
}
console.log('📋 Current RPG Companion State:\n');
console.log('inventoryV3:', JSON.stringify(settings.inventoryV3, null, 2));
console.log('skillsV2:', JSON.stringify(settings.skillsV2, null, 2));
console.log('questsV2:', JSON.stringify(settings.questsV2, null, 2));
console.log('infoBoxData:', JSON.stringify(settings.infoBoxData, null, 2));
console.log('charactersData:', JSON.stringify(settings.charactersData, null, 2));
},
/**
* Help message
*/
help() {
console.log(`
🧪 RPG Companion Test Helper Commands:
RPGTestHelper.validateLastResponse() - Validate current structured data
RPGTestHelper.testJSONExtraction(text) - Test JSON extraction from text
RPGTestHelper.simulateParseResponse(text) - Full parse simulation
RPGTestHelper.printCurrentState() - Print current extension state
RPGTestHelper.help() - Show this help message
Example:
RPGTestHelper.validateLastResponse()
`);
}
};
// Print help on load
console.log('🧪 RPG Companion Test Helper loaded. Run RPGTestHelper.help() for commands.');
+504
View File
@@ -0,0 +1,504 @@
/**
* JSON Format Tests for RPG Companion
*
* These tests can be run in two ways:
* 1. In browser console: Copy/paste or load as module in SillyTavern
* 2. Via Node.js: Run with `node tests/jsonFormat.test.js`
*
* Tests cover:
* - JSON prompt generation
* - JSON response parsing
* - Data structure validation
*/
// Mock SillyTavern context for Node.js testing
const isBrowser = typeof window !== 'undefined';
// Sample mock data for testing
const mockTrackerConfig = {
userStats: {
customStats: [
{ id: 'health', name: 'Health', enabled: true },
{ id: 'energy', name: 'Energy', enabled: true }
],
showRPGAttributes: true,
rpgAttributes: [
{ id: 'str', name: 'Strength', enabled: true },
{ id: 'dex', name: 'Dexterity', enabled: true }
],
statusSection: {
enabled: true,
showMoodEmoji: true,
customFields: ['Conditions']
},
skillsSection: {
enabled: true,
customFields: ['Combat', 'Stealth', 'Magic']
}
},
infoBox: {
widgets: {
date: { enabled: true },
weather: { enabled: true },
temperature: { enabled: true, unit: 'C' },
time: { enabled: true },
location: { enabled: true },
recentEvents: { enabled: true }
}
},
presentCharacters: {
showEmoji: true,
relationshipFields: ['Enemy', 'Neutral', 'Friend', 'Lover'],
customFields: [
{ id: 'appearance', name: 'Appearance', enabled: true },
{ id: 'demeanor', name: 'Demeanor', enabled: true }
],
thoughts: { enabled: true, name: 'Thoughts' },
characterStats: {
enabled: true,
customStats: [
{ id: 'health', name: 'Health', enabled: true },
{ id: 'arousal', name: 'Arousal', enabled: true }
]
}
}
};
// Sample JSON responses for testing parser
const sampleValidJSONResponse = `
Here's an interesting development in the story...
\`\`\`json
{
"userStats": {
"health": 85,
"energy": 60,
"str": 14,
"dex": 12,
"status": {
"mood": "😊",
"conditions": "Well-rested"
}
},
"skills": {
"combat": [
{ "name": "Sword Fighting", "description": "Basic melee combat with swords", "linkedItem": "Iron Sword" },
{ "name": "Parry", "description": "Deflect incoming attacks" }
],
"stealth": [
{ "name": "Sneak", "description": "Move quietly" }
],
"magic": []
},
"inventory": {
"onPerson": [
{ "name": "Iron Sword", "description": "A sturdy blade" },
{ "name": "Leather Armor", "description": "Basic protection" },
{ "name": "Health Potion", "description": "Restores 50 HP" }
],
"stored": {
"Backpack": [
{ "name": "Rope", "description": "50 feet of hemp rope" }
]
},
"assets": [
{ "name": "Small Cottage", "description": "A humble dwelling in the village" }
]
},
"quests": {
"main": "Find the Lost Artifact",
"optional": ["Gather herbs for the healer", "Clear the rat infestation"]
},
"infoBox": {
"date": "15th of Sunstone, Year 1423",
"weather": "☀️ Sunny",
"temperature": "22°C",
"time": "Midday",
"location": "Village Square",
"recentEvents": ["Met the village elder", "Bought supplies"]
},
"presentCharacters": [
{
"name": "Elena",
"description": "A young healer with kind eyes",
"emoji": "😊",
"relationship": "Friend",
"stats": { "health": 100, "arousal": 10 },
"appearance": "Long brown hair, green robes",
"demeanor": "Cheerful and helpful",
"thoughts": "I hope they can help me find the rare herbs..."
}
]
}
\`\`\`
The village was bustling with activity...
`;
const sampleMalformedJSONResponse = `
Some story text here...
\`\`\`json
{
"userStats": {
"health": 85,
"energy": 60,
},
"inventory": {
"onPerson": [
{ "name": "Sword", "description": "Sharp" }
]
}
}
\`\`\`
`;
const sampleSimplifiedInventoryResponse = `
\`\`\`json
{
"userStats": {
"health": 75,
"energy": 50
},
"inventory": {
"simplified": [
{ "name": "Magic Staff", "description": "Channels arcane energy" },
{ "name": "Spell Book", "description": "Contains basic spells" },
{ "name": "Mana Potion", "description": "Restores magical energy" }
]
}
}
\`\`\`
`;
// Test results accumulator
const testResults = {
passed: 0,
failed: 0,
errors: []
};
/**
* Simple assertion helper
*/
function assert(condition, message) {
if (condition) {
testResults.passed++;
console.log(`✅ PASS: ${message}`);
} else {
testResults.failed++;
testResults.errors.push(message);
console.error(`❌ FAIL: ${message}`);
}
}
/**
* Test JSON extraction from markdown code blocks
*/
function testJSONExtraction() {
console.log('\n📋 Testing JSON Extraction from Code Blocks...\n');
// Test 1: Extract valid JSON from code block
const jsonRegex = /```(?:json)?\s*([\s\S]*?)```/i;
const match = sampleValidJSONResponse.match(jsonRegex);
assert(match !== null, 'Should find JSON code block in response');
if (match) {
let parsed;
try {
parsed = JSON.parse(match[1].trim());
assert(true, 'Should parse extracted JSON successfully');
assert(parsed.userStats !== undefined, 'Parsed JSON should have userStats');
assert(parsed.inventory !== undefined, 'Parsed JSON should have inventory');
assert(parsed.skills !== undefined, 'Parsed JSON should have skills');
assert(parsed.quests !== undefined, 'Parsed JSON should have quests');
assert(parsed.infoBox !== undefined, 'Parsed JSON should have infoBox');
assert(parsed.presentCharacters !== undefined, 'Parsed JSON should have presentCharacters');
} catch (e) {
assert(false, `Should not throw parsing error: ${e.message}`);
}
}
// Test 2: Handle malformed JSON (trailing comma)
const malformedMatch = sampleMalformedJSONResponse.match(jsonRegex);
assert(malformedMatch !== null, 'Should find malformed JSON code block');
if (malformedMatch) {
try {
JSON.parse(malformedMatch[1].trim());
assert(false, 'Malformed JSON should throw parsing error');
} catch (e) {
assert(true, 'Malformed JSON correctly throws parsing error');
// Test JSON fixing (remove trailing commas)
const fixed = malformedMatch[1].trim()
.replace(/,\s*}/g, '}')
.replace(/,\s*]/g, ']');
try {
const fixedParsed = JSON.parse(fixed);
assert(fixedParsed.userStats.health === 85, 'Fixed JSON should parse correctly');
} catch (e2) {
assert(false, `JSON fixing should work: ${e2.message}`);
}
}
}
}
/**
* Test inventory data structure validation
*/
function testInventoryStructure() {
console.log('\n📦 Testing Inventory Structure...\n');
const jsonRegex = /```(?:json)?\s*([\s\S]*?)```/i;
const match = sampleValidJSONResponse.match(jsonRegex);
const data = JSON.parse(match[1].trim());
const inv = data.inventory;
// Test onPerson array structure
assert(Array.isArray(inv.onPerson), 'onPerson should be an array');
assert(inv.onPerson.length > 0, 'onPerson should have items');
assert(inv.onPerson[0].name !== undefined, 'Items should have name property');
assert(inv.onPerson[0].description !== undefined, 'Items should have description property');
// Test stored object structure
assert(typeof inv.stored === 'object', 'stored should be an object');
assert(inv.stored.Backpack !== undefined, 'stored should have location keys');
assert(Array.isArray(inv.stored.Backpack), 'stored locations should be arrays');
// Test assets array
assert(Array.isArray(inv.assets), 'assets should be an array');
// Test simplified inventory
const simplifiedMatch = sampleSimplifiedInventoryResponse.match(jsonRegex);
const simplifiedData = JSON.parse(simplifiedMatch[1].trim());
assert(Array.isArray(simplifiedData.inventory.simplified), 'simplified inventory should be an array');
}
/**
* Test skills data structure validation
*/
function testSkillsStructure() {
console.log('\n⚔️ Testing Skills Structure...\n');
const jsonRegex = /```(?:json)?\s*([\s\S]*?)```/i;
const match = sampleValidJSONResponse.match(jsonRegex);
const data = JSON.parse(match[1].trim());
const skills = data.skills;
// Test skill categories
assert(typeof skills === 'object', 'skills should be an object');
assert(skills.combat !== undefined, 'skills should have combat category');
assert(Array.isArray(skills.combat), 'skill categories should be arrays');
// Test ability structure
const ability = skills.combat[0];
assert(ability.name !== undefined, 'Abilities should have name');
assert(ability.description !== undefined, 'Abilities should have description');
// Test linked item
assert(ability.linkedItem === 'Iron Sword', 'First combat ability should be linked to Iron Sword');
assert(skills.combat[1].linkedItem === undefined || skills.combat[1].linkedItem === null,
'Second combat ability should not have linkedItem');
}
/**
* Test quests data structure
*/
function testQuestsStructure() {
console.log('\n📜 Testing Quests Structure...\n');
const jsonRegex = /```(?:json)?\s*([\s\S]*?)```/i;
const match = sampleValidJSONResponse.match(jsonRegex);
const data = JSON.parse(match[1].trim());
const quests = data.quests;
assert(typeof quests.main === 'string', 'main quest should be a string');
assert(Array.isArray(quests.optional), 'optional quests should be an array');
assert(quests.optional.length === 2, 'Should have 2 optional quests');
}
/**
* Test characters data structure
*/
function testCharactersStructure() {
console.log('\n👥 Testing Characters Structure...\n');
const jsonRegex = /```(?:json)?\s*([\s\S]*?)```/i;
const match = sampleValidJSONResponse.match(jsonRegex);
const data = JSON.parse(match[1].trim());
const chars = data.presentCharacters;
assert(Array.isArray(chars), 'presentCharacters should be an array');
assert(chars.length > 0, 'Should have at least one character');
const char = chars[0];
assert(char.name === 'Elena', 'Character should have name');
assert(char.description !== undefined, 'Character should have description');
assert(char.emoji !== undefined, 'Character should have emoji');
assert(char.relationship !== undefined, 'Character should have relationship');
assert(char.stats !== undefined, 'Character should have stats');
assert(char.thoughts !== undefined, 'Character should have thoughts');
}
/**
* Test info box data structure
*/
function testInfoBoxStructure() {
console.log('\n📍 Testing Info Box Structure...\n');
const jsonRegex = /```(?:json)?\s*([\s\S]*?)```/i;
const match = sampleValidJSONResponse.match(jsonRegex);
const data = JSON.parse(match[1].trim());
const info = data.infoBox;
assert(typeof info.date === 'string', 'date should be a string');
assert(typeof info.weather === 'string', 'weather should be a string');
assert(typeof info.temperature === 'string', 'temperature should be a string');
assert(typeof info.time === 'string', 'time should be a string');
assert(typeof info.location === 'string', 'location should be a string');
assert(Array.isArray(info.recentEvents), 'recentEvents should be an array');
}
/**
* Test JSON prompt schema generation (mock)
*/
function testPromptSchemaGeneration() {
console.log('\n📝 Testing Prompt Schema Generation...\n');
// This tests the expected schema structure that generateJSONTrackerInstructions should produce
const expectedSchemaProperties = [
'userStats',
'skills',
'inventory',
'quests',
'infoBox',
'presentCharacters'
];
// Mock schema generation based on config
const schema = {
type: 'object',
properties: {}
};
// User stats
if (mockTrackerConfig.userStats) {
schema.properties.userStats = { type: 'object', properties: {} };
// Custom stats
mockTrackerConfig.userStats.customStats.forEach(stat => {
if (stat.enabled) {
schema.properties.userStats.properties[stat.id] = {
type: 'integer',
minimum: 0,
maximum: 100
};
}
});
// RPG attributes
if (mockTrackerConfig.userStats.showRPGAttributes) {
mockTrackerConfig.userStats.rpgAttributes.forEach(attr => {
if (attr.enabled) {
schema.properties.userStats.properties[attr.id] = {
type: 'integer',
minimum: 1
};
}
});
}
}
// Skills
if (mockTrackerConfig.userStats.skillsSection?.enabled) {
schema.properties.skills = { type: 'object', properties: {} };
mockTrackerConfig.userStats.skillsSection.customFields.forEach(field => {
const fieldId = field.toLowerCase();
schema.properties.skills.properties[fieldId] = {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
description: { type: 'string' },
linkedItem: { type: 'string', nullable: true }
}
}
};
});
}
// Inventory
schema.properties.inventory = {
type: 'object',
properties: {
onPerson: { type: 'array' },
stored: { type: 'object' },
assets: { type: 'array' }
}
};
// Validate schema structure
assert(schema.properties.userStats !== undefined, 'Schema should include userStats');
assert(schema.properties.userStats.properties.health !== undefined, 'Schema should include health stat');
assert(schema.properties.skills !== undefined, 'Schema should include skills');
assert(schema.properties.skills.properties.combat !== undefined, 'Schema should include combat skill');
assert(schema.properties.inventory !== undefined, 'Schema should include inventory');
console.log('Generated schema structure:', JSON.stringify(schema, null, 2).substring(0, 500) + '...');
}
/**
* Run all tests
*/
function runAllTests() {
console.log('🧪 RPG Companion JSON Format Tests\n');
console.log('='.repeat(50));
try {
testJSONExtraction();
testInventoryStructure();
testSkillsStructure();
testQuestsStructure();
testCharactersStructure();
testInfoBoxStructure();
testPromptSchemaGeneration();
} catch (e) {
console.error('💥 Test suite error:', e);
testResults.failed++;
testResults.errors.push(e.message);
}
console.log('\n' + '='.repeat(50));
console.log(`\n📊 Results: ${testResults.passed} passed, ${testResults.failed} failed`);
if (testResults.errors.length > 0) {
console.log('\n❌ Failed tests:');
testResults.errors.forEach(err => console.log(` - ${err}`));
}
return testResults;
}
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
runAllTests,
sampleValidJSONResponse,
sampleMalformedJSONResponse,
sampleSimplifiedInventoryResponse,
mockTrackerConfig
};
}
// Auto-run if executed directly
if (!isBrowser || (isBrowser && window.RPG_RUN_TESTS)) {
runAllTests();
}
+342
View File
@@ -0,0 +1,342 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RPG Companion - Test Runner</title>
<style>
:root {
--bg: #1a1a2e;
--surface: #16213e;
--border: #0f3460;
--text: #e8e8e8;
--accent: #e94560;
--success: #4caf50;
--error: #f44336;
--warning: #ff9800;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
padding: 2rem;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
}
h1 {
color: var(--accent);
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
h1::before {
content: '🧪';
}
.description {
background: var(--surface);
padding: 1rem;
border-radius: 8px;
margin-bottom: 2rem;
border-left: 4px solid var(--accent);
}
.controls {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
}
button {
background: var(--accent);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.2s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(233, 69, 96, 0.3);
}
button.secondary {
background: var(--surface);
border: 1px solid var(--border);
}
.results-summary {
display: flex;
gap: 2rem;
margin-bottom: 2rem;
padding: 1rem;
background: var(--surface);
border-radius: 8px;
}
.stat {
display: flex;
flex-direction: column;
align-items: center;
}
.stat-value {
font-size: 2.5rem;
font-weight: bold;
}
.stat-label {
font-size: 0.85rem;
opacity: 0.7;
text-transform: uppercase;
}
.stat.passed .stat-value { color: var(--success); }
.stat.failed .stat-value { color: var(--error); }
.stat.total .stat-value { color: var(--accent); }
.test-output {
background: #0d1117;
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem;
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 0.9rem;
max-height: 500px;
overflow-y: auto;
}
.test-output .pass { color: var(--success); }
.test-output .fail { color: var(--error); }
.test-output .section {
color: var(--accent);
font-weight: bold;
margin-top: 1rem;
}
.test-output .info { color: #8b949e; }
.test-section {
margin-bottom: 2rem;
}
h2 {
color: var(--accent);
margin-bottom: 1rem;
font-size: 1.25rem;
}
.json-sample {
background: #0d1117;
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem;
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 0.8rem;
overflow-x: auto;
white-space: pre;
}
.tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.tab {
padding: 0.5rem 1rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px 6px 0 0;
cursor: pointer;
transition: all 0.2s;
}
.tab.active {
background: var(--accent);
border-color: var(--accent);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.note {
background: rgba(255, 152, 0, 0.1);
border: 1px solid var(--warning);
padding: 1rem;
border-radius: 8px;
margin-top: 1rem;
}
.note::before {
content: '⚠️ ';
}
</style>
</head>
<body>
<div class="container">
<h1>RPG Companion Test Runner</h1>
<div class="description">
<p>This test suite validates the JSON format used for structured tracker data.</p>
<p>Tests cover: JSON extraction, parsing, data structure validation, and schema generation.</p>
</div>
<div class="controls">
<button onclick="runTests()">▶️ Run All Tests</button>
<button class="secondary" onclick="clearResults()">🗑️ Clear Results</button>
</div>
<div class="results-summary" id="results-summary" style="display: none;">
<div class="stat total">
<span class="stat-value" id="total-count">0</span>
<span class="stat-label">Total</span>
</div>
<div class="stat passed">
<span class="stat-value" id="passed-count">0</span>
<span class="stat-label">Passed</span>
</div>
<div class="stat failed">
<span class="stat-value" id="failed-count">0</span>
<span class="stat-label">Failed</span>
</div>
</div>
<div class="test-section">
<h2>Test Output</h2>
<div class="test-output" id="test-output">
<span class="info">Click "Run All Tests" to start...</span>
</div>
</div>
<div class="tabs">
<div class="tab active" onclick="showTab('valid')">Valid JSON</div>
<div class="tab" onclick="showTab('malformed')">Malformed JSON</div>
<div class="tab" onclick="showTab('simplified')">Simplified Inventory</div>
</div>
<div id="tab-valid" class="tab-content active">
<h2>Sample Valid JSON Response</h2>
<div class="json-sample" id="valid-json"></div>
</div>
<div id="tab-malformed" class="tab-content">
<h2>Sample Malformed JSON (with trailing comma)</h2>
<div class="json-sample" id="malformed-json"></div>
</div>
<div id="tab-simplified" class="tab-content">
<h2>Sample Simplified Inventory Response</h2>
<div class="json-sample" id="simplified-json"></div>
</div>
<div class="note">
<strong>Note:</strong> These tests run in isolation and don't require SillyTavern to be running.
For integration testing with actual LLM responses, use the Debug Mode in the extension settings.
</div>
</div>
<script type="module">
// Import test module
import {
runAllTests,
sampleValidJSONResponse,
sampleMalformedJSONResponse,
sampleSimplifiedInventoryResponse
} from './jsonFormat.test.js';
// Display sample JSON
document.getElementById('valid-json').textContent = sampleValidJSONResponse;
document.getElementById('malformed-json').textContent = sampleMalformedJSONResponse;
document.getElementById('simplified-json').textContent = sampleSimplifiedInventoryResponse;
// Override console for capturing test output
const originalLog = console.log;
const originalError = console.error;
let outputBuffer = [];
function captureConsole() {
outputBuffer = [];
console.log = (...args) => {
outputBuffer.push({ type: 'log', message: args.join(' ') });
originalLog.apply(console, args);
};
console.error = (...args) => {
outputBuffer.push({ type: 'error', message: args.join(' ') });
originalError.apply(console, args);
};
}
function restoreConsole() {
console.log = originalLog;
console.error = originalError;
}
function formatOutput(buffer) {
return buffer.map(item => {
let cls = 'info';
if (item.message.includes('✅')) cls = 'pass';
else if (item.message.includes('❌')) cls = 'fail';
else if (item.message.includes('📋') || item.message.includes('📦') ||
item.message.includes('⚔️') || item.message.includes('📜') ||
item.message.includes('👥') || item.message.includes('📍') ||
item.message.includes('📝')) cls = 'section';
return `<div class="${cls}">${escapeHtml(item.message)}</div>`;
}).join('');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
window.runTests = function() {
captureConsole();
const results = runAllTests();
restoreConsole();
document.getElementById('test-output').innerHTML = formatOutput(outputBuffer);
document.getElementById('results-summary').style.display = 'flex';
document.getElementById('total-count').textContent = results.passed + results.failed;
document.getElementById('passed-count').textContent = results.passed;
document.getElementById('failed-count').textContent = results.failed;
};
window.clearResults = function() {
document.getElementById('test-output').innerHTML = '<span class="info">Click "Run All Tests" to start...</span>';
document.getElementById('results-summary').style.display = 'none';
};
window.showTab = function(tabId) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
document.getElementById('tab-' + tabId).classList.add('active');
};
</script>
</body>
</html>