feat: json format, et al.
This commit is contained in:
@@ -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.');
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user