From 67303168bf0f78eb635612aec4da3e7fa03a02cf Mon Sep 17 00:00:00 2001 From: "courtmanr@gmail.com" Date: Mon, 17 Mar 2025 16:53:18 +0000 Subject: [PATCH] test: add additional search test utilities --- frontend/src/utils/ExhaustiveSearchTests.js | 374 ++++++++++++++++++++ frontend/src/utils/VerifyAllSearches.js | 79 +++++ frontend/src/utils/debugSearch.js | 173 +++++++++ frontend/src/utils/priSearchTest.js | 193 ++++++++++ 4 files changed, 819 insertions(+) create mode 100644 frontend/src/utils/ExhaustiveSearchTests.js create mode 100644 frontend/src/utils/VerifyAllSearches.js create mode 100644 frontend/src/utils/debugSearch.js create mode 100644 frontend/src/utils/priSearchTest.js diff --git a/frontend/src/utils/ExhaustiveSearchTests.js b/frontend/src/utils/ExhaustiveSearchTests.js new file mode 100644 index 000000000..d4090b416 --- /dev/null +++ b/frontend/src/utils/ExhaustiveSearchTests.js @@ -0,0 +1,374 @@ +/** + * Exhaustive Search Test Suite + * + * This test suite verifies ALL possible search patterns, combinations, and edge cases. + * It uses both mock data and real-world data structures to ensure accuracy. + */ + +const { getSortedAndFilteredData } = require('./networkUtils'); + +// Create a more extensive mock data set with varied properties +const generateExhaustiveTestData = () => { + // Base test data similar to the real application + const guests = [ + // PRIMARY GUESTS + { + id: '101', + name: 'web-server', // Plain name + type: 'qemu', + status: 'running', + node: 'pve-prod-01', + shared: true, + primaryNode: 'pve-prod-01', + tags: ['web', 'production'] + }, + { + id: '102', + name: 'database-primary', // Has "primary" in name + type: 'qemu', + status: 'running', + node: 'pve-prod-01', + shared: true, + primaryNode: 'pve-prod-01', + tags: ['db', 'primary', 'production'] + }, + { + id: '103', + name: 'sprint-server', // Has "pri" as substring + type: 'qemu', + status: 'paused', + node: 'pve-prod-01', + shared: true, + primaryNode: 'pve-prod-01', + tags: ['sprint', 'production'] + }, + + // SECONDARY GUESTS + { + id: '201', + name: 'web-secondary', + type: 'qemu', + status: 'running', + node: 'pve-prod-02', + shared: true, + primaryNode: 'pve-prod-01', + tags: ['web', 'secondary'] + }, + { + id: '202', + name: 'sec-database', // Has "sec" at start of name + type: 'lxc', + status: 'stopped', + node: 'pve-prod-02', + shared: true, + primaryNode: 'pve-prod-01', + tags: ['db', 'backup'] + }, + { + id: '203', + name: 'prism-backup', // Has "pri" as substring + type: 'lxc', + status: 'running', + node: 'pve-prod-02', + shared: true, + primaryNode: 'pve-prod-01', + tags: ['backup'] + }, + + // NON-SHARED GUESTS + { + id: '301', + name: 'standalone-pri-app', // Has "pri" in name but is NOT primary + type: 'qemu', + status: 'running', + node: 'pve-dev-01', + shared: false, + tags: ['dev', 'private'] + }, + { + id: '302', + name: 'security-container', // Has "sec" as substring but is NOT secondary + type: 'lxc', + status: 'stopped', + node: 'pve-dev-01', + shared: false, + tags: ['security', 'dev'] + }, + + // EDGE CASES + { + id: '401', + name: 'p', // Single-letter name matching a role search + type: 'qemu', + status: 'running', + node: 'pve-edge-01', + shared: true, + primaryNode: 'pve-edge-01', + tags: ['test'] + }, + { + id: '402', + name: 's', // Single-letter name matching a role search + type: 'qemu', + status: 'running', + node: 'pve-edge-01', + shared: true, + primaryNode: 'pve-prod-01', + tags: ['test'] + } + ]; + + // Add optional metrics data + const metricsData = { + cpu: {}, + memory: {}, + disk: {}, + network: {} + }; + + guests.forEach(guest => { + const id = guest.id; + metricsData.cpu[id] = { usage: Math.random() * 100 }; + metricsData.memory[id] = { usagePercent: Math.random() * 100 }; + metricsData.disk[id] = { usagePercent: Math.random() * 100 }; + metricsData.network[id] = { inRate: Math.random() * 10000000, outRate: Math.random() * 10000000 }; + }); + + // Node data for name resolution + const nodeData = [ + { id: 'pve-prod-01', name: 'prod-cluster-1' }, + { id: 'pve-prod-02', name: 'prod-cluster-2' }, + { id: 'pve-dev-01', name: 'dev-cluster' }, + { id: 'pve-edge-01', name: 'edge-node' } + ]; + + return { guests, metricsData, nodeData }; +}; + +// Function to run all exhaustive tests +export function runExhaustiveTests() { + console.log('===== EXHAUSTIVE SEARCH TEST SUITE ====='); + console.log('Testing all possible search patterns and edge cases'); + + const { guests, metricsData, nodeData } = generateExhaustiveTestData(); + + // Collection of all test cases + const testCases = [ + // 1. ROLE BASED SEARCHES - STANDALONE TERMS + // Each of these tests a specific role search term + { category: 'ROLE STANDALONE', term: 'role', description: 'All shared guests', + expectIdsContaining: ['101', '102', '103', '201', '202', '203', '401', '402'] }, + + { category: 'ROLE STANDALONE', term: 'shared', description: 'Alternative for all shared guests', + expectIdsContaining: ['101', '102', '103', '201', '202', '203', '401', '402'] }, + + { category: 'ROLE STANDALONE', term: 'primary', description: 'Finds primary guests', + expectIdsContaining: ['101', '102', '103', '401'] }, + + { category: 'ROLE STANDALONE', term: 'pri', description: 'Short form of primary', + expectIdsContaining: ['101', '102', '103', '401'] }, + + { category: 'ROLE STANDALONE', term: 'p', description: 'Shortest form of primary', + expectIdsContaining: ['101', '102', '103', '401'] }, + + { category: 'ROLE STANDALONE', term: 'secondary', description: 'Finds secondary guests', + expectIdsContaining: ['201', '202', '203', '402'] }, + + { category: 'ROLE STANDALONE', term: 'sec', description: 'Short form of secondary', + expectIdsContaining: ['201', '202', '203', '402'] }, + + { category: 'ROLE STANDALONE', term: 's', description: 'Shortest form of secondary', + expectIdsContaining: ['201', '202', '203', '402'] }, + + // 2. ROLE COLUMN SEARCHES + { category: 'ROLE COLUMN', term: 'role:primary', description: 'Column search for primary', + expectIdsContaining: ['101', '102', '103', '401'] }, + + { category: 'ROLE COLUMN', term: 'role:pri', description: 'Column search with short primary', + expectIdsContaining: ['101', '102', '103', '401'] }, + + { category: 'ROLE COLUMN', term: 'role:p', description: 'Column search with shortest primary', + expectIdsContaining: ['101', '102', '103', '401'] }, + + { category: 'ROLE COLUMN', term: 'role:secondary', description: 'Column search for secondary', + expectIdsContaining: ['201', '202', '203', '402'] }, + + { category: 'ROLE COLUMN', term: 'role:sec', description: 'Column search with short secondary', + expectIdsContaining: ['201', '202', '203', '402'] }, + + { category: 'ROLE COLUMN', term: 'role:s', description: 'Column search with shortest secondary', + expectIdsContaining: ['201', '202', '203', '402'] }, + + { category: 'ROLE COLUMN', term: 'role:none', description: 'Column search for non-shared', + expectIdsContaining: ['301', '302'] }, + + { category: 'ROLE COLUMN', term: 'role:-', description: 'Column search for non-shared (dash)', + expectIdsContaining: ['301', '302'] }, + + // 3. TEXT SEARCHES WITH ROLE COMPONENTS + { category: 'TEXT MATCH', term: 'primary-', description: 'Has primary in name but with a suffix', + expectIdsContaining: ['102'] }, + + { category: 'TEXT MATCH', term: 'sprint', description: 'Has pri in middle of name but not as a word', + expectIdsContaining: ['103'] }, + + { category: 'TEXT MATCH', term: 'secondary', description: 'Has secondary in name and/or is secondary', + expectIdsContaining: ['201', '202', '203', '402'] }, + + { category: 'TEXT MATCH', term: 'prism', description: 'Has pri as substring', + expectIdsContaining: ['203'] }, + + // 4. PARTIAL SEARCHES AND EDGE CASES + // The 'p' search should match shared guests with primary role AND match 'p' in text + { category: 'SINGLE CHAR', term: 'p', description: 'Single letter p', + expectIdsContaining: ['101', '102', '103', '201', '202', '203', '301', '302', '401', '402'] }, + + // Same for 's' + { category: 'SINGLE CHAR', term: 's', description: 'Single letter s', + expectIdsContaining: ['101', '102', '103', '201', '202', '203', '301', '302', '401', '402'] }, + + // Test pve prefix in node names + { category: 'SINGLE CHAR', term: 'pve', description: 'Text in all node IDs', + expectIdsContaining: ['101', '102', '103', '201', '202', '203', '301', '302', '401', '402'] }, + + // Test a mix of properties and values + { category: 'TYPE SEARCH', term: 'qemu', description: 'VM type search', + expectIdsContaining: ['101', '102', '103', '201', '301', '401', '402'] }, + + { category: 'TYPE SEARCH', term: 'vm', description: 'VM alternate search', + expectIdsContaining: ['101', '102', '103', '201', '301', '401', '402'] }, + + { category: 'TYPE SEARCH', term: 'lxc', description: 'Container type search', + expectIdsContaining: ['202', '203', '302'] }, + + // 5. COMBINED SEARCHES (AND LOGIC) + { category: 'COMBINED', term: 'primary running', description: 'Primary AND running status', + expectIdsContaining: ['101', '102', '401'] }, + + { category: 'COMBINED', term: 'sec stopped', description: 'Secondary AND stopped status', + expectIdsContaining: ['202'] }, + + // 6. NEGATIVE TESTS - these should NOT match + { category: 'NEGATIVE', term: 'nonexistent', description: 'Term that appears nowhere', + expectIdsContaining: [] } + ]; + + // Run the tests + console.log(`Running ${testCases.length} comprehensive search test cases\n`); + + let passCount = 0; + let failCount = 0; + + // Keep track of failures by category + const failures = {}; + + testCases.forEach((testCase, index) => { + console.log(`[${index + 1}/${testCases.length}] Testing "${testCase.term}" - ${testCase.description}`); + + // Run the search + const filteredData = getSortedAndFilteredData( + guests, + { key: 'name', direction: 'asc' }, // Default sort + {}, // No filters + null, // Show all statuses + [testCase.term], // Single search term as an array + '', // No active search term + metricsData, + 'all', // Show all guest types + nodeData + ); + + // Extract result IDs + const resultIds = filteredData.map(guest => guest.id); + + // Validate expected IDs are included + const expectedIds = testCase.expectIdsContaining; + const missingIds = expectedIds.filter(id => !resultIds.includes(id)); + const unexpectedIds = resultIds.filter(id => !expectedIds.includes(id)); + + const passed = missingIds.length === 0 && + (unexpectedIds.length === 0 || expectedIds.length === 0 && resultIds.length === 0); + + if (passed) { + console.log(` ✅ PASSED - Found ${resultIds.length} guests as expected`); + passCount++; + } else { + console.log(` ❌ FAILED`); + if (missingIds.length > 0) { + console.log(` Missing IDs: ${missingIds.join(', ')}`); + } + if (unexpectedIds.length > 0) { + console.log(` Unexpected IDs: ${unexpectedIds.join(', ')}`); + } + + // Add to failures by category + const category = testCase.category; + if (!failures[category]) { + failures[category] = []; + } + failures[category].push({ + term: testCase.term, + description: testCase.description, + expected: expectedIds, + actual: resultIds, + missing: missingIds, + unexpected: unexpectedIds + }); + + failCount++; + } + + console.log(''); // Empty line for readability + }); + + // Print summary + console.log('===== TEST SUMMARY ====='); + console.log(`Total tests: ${testCases.length}`); + console.log(`Passed: ${passCount} (${(passCount/testCases.length*100).toFixed(1)}%)`); + console.log(`Failed: ${failCount} (${(failCount/testCases.length*100).toFixed(1)}%)`); + + // Print failures by category + if (failCount > 0) { + console.log('\n===== FAILURES BY CATEGORY ====='); + Object.keys(failures).forEach(category => { + console.log(`\n${category} - ${failures[category].length} failures:`); + failures[category].forEach(failure => { + console.log(` "${failure.term}" - ${failure.description}`); + console.log(` Expected: ${failure.expected.join(', ')}`); + console.log(` Actual: ${failure.actual.join(', ')}`); + }); + }); + + // Provide advice for fixing issues + console.log('\n===== TROUBLESHOOTING ====='); + + // Check specifically for 'pri' issues + if (failures['ROLE STANDALONE']?.some(f => f.term === 'pri')) { + console.log('\nIssue detected with "pri" searches:'); + console.log('1. Check if "pri" is being treated as a special case rather than a role indicator'); + console.log('2. Ensure proper handling of "pri" at the beginning of the search logic'); + console.log('3. Make sure word boundaries are properly enforced for "pri" matches'); + } + + // Check for single character issues + if (Object.keys(failures).includes('SINGLE CHAR')) { + console.log('\nIssue detected with single character searches:'); + console.log('1. Single character searches should perform a full text search across all fields'); + console.log('2. Check that single character logic runs BEFORE role-specific logic'); + } + } + + return { + totalTests: testCases.length, + passed: passCount, + failed: failCount, + failures + }; +} + +// Run the tests if executed directly +if (typeof require !== 'undefined' && require.main === module) { + runExhaustiveTests(); +} + +module.exports = { runExhaustiveTests }; \ No newline at end of file diff --git a/frontend/src/utils/VerifyAllSearches.js b/frontend/src/utils/VerifyAllSearches.js new file mode 100644 index 000000000..e884d47e5 --- /dev/null +++ b/frontend/src/utils/VerifyAllSearches.js @@ -0,0 +1,79 @@ +/** + * Comprehensive Search Verification Script + * + * This script runs a battery of tests to verify that ALL search patterns work correctly. + * It combines both standard tests and special test cases for edge conditions. + */ + +import { runSearchTests } from './searchTests.js'; + +console.log('==============================================================='); +console.log('= COMPREHENSIVE SEARCH VERIFICATION ='); +console.log('==============================================================='); +console.log('This script verifies ALL search patterns work correctly, including:'); +console.log('- Basic text searches'); +console.log('- Role-specific searches (pri, sec, primary, secondary)'); +console.log('- Column-based searches (role:pri, type:vm, etc.)'); +console.log('- Single character searches (p, s, v, etc.)'); +console.log('- Edge cases and potentially problematic patterns'); +console.log('\nRunning standard search test suite...'); + +// Execute all standard tests +const results = runSearchTests(); + +// Output summary +console.log('\n==============================================================='); +console.log('= VERIFICATION RESULTS ='); +console.log('==============================================================='); + +if (results.general.failed === 0 && results.role.filter(r => !r.passed).length === 0) { + console.log('✅ ALL TESTS PASSED!'); + console.log(` - ${results.general.passed} general tests passed`); + console.log(` - ${results.role.filter(r => r.passed).length} role-specific tests passed`); + console.log('\nThe search functionality is working correctly for all patterns.'); + console.log('Key validations:'); + console.log(' - "pri" correctly returns primary guests'); + console.log(' - "sec" correctly returns secondary guests'); + console.log(' - Single-character searches work properly'); + console.log(' - Combined searches apply AND logic correctly'); +} else { + console.log('❌ SOME TESTS FAILED!'); + console.log(` - ${results.general.failed} general tests failed`); + console.log(` - ${results.role.filter(r => !r.passed).length} role-specific tests failed`); + + // Show detailed failures + console.log('\nFailed tests:'); + if (results.general.failed > 0) { + results.general.details + .filter(detail => !detail.passed) + .forEach(detail => { + console.log(` - ${detail.name}: ${detail.description}`); + console.log(` Expected: ${JSON.stringify(detail.expectedIds)}`); + console.log(` Actual: ${JSON.stringify(detail.actualIds)}`); + }); + } + + const failedRoleTests = results.role.filter(r => !r.passed); + if (failedRoleTests.length > 0) { + console.log('\nFailed role-specific tests:'); + failedRoleTests.forEach(r => { + console.log(` - "${r.term}": Expected ${JSON.stringify(r.expectedIds)}, got ${JSON.stringify(r.resultIds)}`); + }); + } +} + +// Execution instructions +console.log('\n==============================================================='); +console.log('= HOW TO USE THIS VERIFICATION TOOL ='); +console.log('==============================================================='); +console.log('Run this script after making any changes to the search functionality:'); +console.log(' node frontend/src/utils/VerifyAllSearches.js'); +console.log('\nIf you add new search capabilities, update searchTests.js to include tests'); +console.log('for the new functionality.'); + +// Exit with appropriate code +if (results.general.failed === 0 && results.role.filter(r => !r.passed).length === 0) { + process.exit(0); +} else { + process.exit(1); +} \ No newline at end of file diff --git a/frontend/src/utils/debugSearch.js b/frontend/src/utils/debugSearch.js new file mode 100644 index 000000000..e97dd999e --- /dev/null +++ b/frontend/src/utils/debugSearch.js @@ -0,0 +1,173 @@ +// Debug script for testing 'pri' search issue + +// Mock implementation of matchesTerm function for testing +function matchesTerm(guest, termLower, nodeData) { + // Prevent operations on undefined/null terms + if (!termLower) return true; + + // CASE 3: Standard role terminology + // Matches exact role terms (not as part of other words) using standard terminology + if (termLower === 'shared' || termLower === 'role') { + return !!guest.shared; + } + + // Primary role terms: match whole words only, requires shared=true and isPrimary=true + if (termLower === 'primary' || termLower === 'pri') { + if (!guest.shared) return false; + console.log(`Checking primary for ${guest.id}: primaryNode=${guest.primaryNode}, node=${guest.node}, matches=${guest.primaryNode === guest.node}`); + return guest.primaryNode === guest.node; + } + + // Secondary role terms: match whole words only, requires shared=true and isPrimary=false + if (termLower === 'secondary' || termLower === 'sec') { + if (!guest.shared) return false; + return guest.primaryNode !== guest.node; + } + + // CASE 5: Single character searches (including role abbreviations) + if (termLower.length === 1) { + const searchText = getFullSearchableText(guest, nodeData); + console.log(`Single-char search for ${termLower} in ${guest.id}: "${searchText}"`); + return searchText.includes(termLower); + } + + // Default: search in all text fields + const searchText = getFullSearchableText(guest, nodeData); + console.log(`Full search for ${termLower} in ${guest.id}: "${searchText}"`); + return searchText.includes(termLower); +} + +// Improved function to get complete searchable text for a guest +function getFullSearchableText(guest, nodeData) { + // Include ALL searchable properties + const nodeName = guest.node || ''; + + // Build full searchable text by concatenating ALL searchable fields + const searchableFields = [ + guest.name || '', + guest.id || '', + guest.status || '', + // Add VM/CT descriptive terms + guest.type === 'qemu' ? 'vm virtual machine' : 'ct container', + nodeName, + // Add role descriptive terms if shared + guest.shared ? (guest.primaryNode === guest.node ? 'primary pri p' : 'secondary sec s') : '', + // Add shared indicator if applicable + guest.shared ? 'shared role' : 'none', + // Any other custom properties that should be searchable + guest.description || '', + guest.tags || '' + ]; + + // Join all fields with spaces and convert to lowercase + const fullSearchText = searchableFields.join(' ').toLowerCase(); + return fullSearchText; +} + +// Mock test data - simplified version of actual app data +const testGuests = [ + { + id: '101', + name: 'web-server', + status: 'running', + type: 'qemu', + node: 'pve-prod-01', + shared: true, + primaryNode: 'pve-prod-01' // This is primary on this node + }, + { + id: '102', + name: 'database', + status: 'running', + type: 'qemu', + node: 'pve-prod-01', + shared: true, + primaryNode: 'pve-prod-01' // This is primary on this node + }, + { + id: '201', + name: 'cache-server', + status: 'stopped', + type: 'qemu', + node: 'pve-prod-02', + shared: true, + primaryNode: 'pve-prod-01' // This is secondary on this node + } +]; + +// Mock node data +const nodeData = [ + { id: 'pve-prod-01', name: 'Production Node 1' }, + { id: 'pve-prod-02', name: 'Production Node 2' } +]; + +// Test specific search terms directly +function testSearch(searchTerm) { + console.log(`\n===== TESTING SEARCH TERM: "${searchTerm}" =====`); + + const result = testGuests.filter(guest => { + const matches = matchesTerm(guest, searchTerm.toLowerCase(), nodeData); + console.log(` Guest ${guest.id} (${guest.name}) matches ${searchTerm}? ${matches ? 'YES' : 'NO'}`); + return matches; + }); + + console.log(`\nResults for "${searchTerm}":`); + console.log(` Found ${result.length} guests:`); + + if (result.length === 0) { + console.log(" NO RESULTS FOUND!"); + } else { + result.forEach(guest => { + console.log(` - ${guest.id}: ${guest.name} (${guest.node}, shared=${guest.shared}, primaryNode=${guest.primaryNode})`); + console.log(` Primary? ${guest.primaryNode === guest.node ? 'YES' : 'NO'}`); + }); + } + + // Print expected vs actual for primaries + const expectedPrimaries = testGuests.filter(g => g.shared && g.primaryNode === g.node); + console.log(`\nExpected primaries: ${expectedPrimaries.length} guests`); + expectedPrimaries.forEach(g => console.log(` - ${g.id}: ${g.name}`)); + + // Verify if all primaries were found + const allPrimariesFound = expectedPrimaries.every( + expected => result.some(res => res.id === expected.id) + ); + + console.log(`\nAll primaries found? ${allPrimariesFound ? 'YES ✅' : 'NO ❌'}`); + + if (!allPrimariesFound) { + console.log("Missing primaries:"); + expectedPrimaries.forEach(expected => { + if (!result.some(res => res.id === expected.id)) { + console.log(` - ${expected.id}: ${expected.name}`); + } + }); + } +} + +// Test various forms of the primary search +console.log("\n********** DEBUGGING 'PRI' SEARCH ISSUE **********"); +testSearch('pri'); +testSearch('primary'); +testSearch('p'); // Single character test + +// Test role-prefixed searches to check if they work differently +console.log("\n********** TESTING PREFIXED SEARCHES **********"); +testSearch('role:pri'); +testSearch('role:primary'); + +console.log("\n\n********** SEARCH IMPLEMENTATION DETAILS **********"); +// Print the relevant implementation from networkUtils.js +console.log(` +Search logic for 'pri' is implemented in networkUtils.js: + +// Standard role terminology +if (termLower === 'primary' || termLower === 'pri') { + if (!guest.shared) return false; + return guest.primaryNode === guest.node; +} + +and in getFullSearchableText: + +guest.shared ? (guest.primaryNode === guest.node ? 'primary pri p' : 'secondary sec s') : '', +`); \ No newline at end of file diff --git a/frontend/src/utils/priSearchTest.js b/frontend/src/utils/priSearchTest.js new file mode 100644 index 000000000..ac43e0cec --- /dev/null +++ b/frontend/src/utils/priSearchTest.js @@ -0,0 +1,193 @@ +// Standalone test for 'pri' search +// Simple script to test that 'pri' search works properly + +// Mock implementation of required functions +function matchesTerm(guest, termLower, nodeData) { + // Prevent operations on undefined/null terms + if (!termLower) return true; + + // Handle column-specific searches + if (termLower.includes(':')) { + const [prefix, value] = termLower.split(':', 2); + + if (prefix.trim().toLowerCase() === 'role') { + const roleValue = (value || '').trim().toLowerCase(); + + // Non-shared checks + if (roleValue === '-' || roleValue === 'none') { + return !guest.shared; + } + + // Need to be shared for other role searches + if (!guest.shared) return false; + + const isPrimary = guest.primaryNode === guest.node; + + // Primary checks + if (roleValue === 'p' || roleValue.startsWith('pri') || roleValue === 'primary') { + console.log(`Column search ${termLower} for ${guest.id}: isPrimary=${isPrimary}`); + return isPrimary; + } + + // Secondary checks + if (roleValue === 's' || roleValue.startsWith('sec') || roleValue === 'secondary') { + return !isPrimary; + } + + return false; + } + + // Unknown column, just do text search + return getFullSearchableText(guest).includes(termLower); + } + + // Standard role terminology + if (termLower === 'primary' || termLower === 'pri') { + console.log(`Standard term "${termLower}" check for ${guest.id}: shared=${guest.shared}, isPrimary=${guest.primaryNode === guest.node}`); + if (!guest.shared) return false; + return guest.primaryNode === guest.node; + } + + // Secondary role terms + if (termLower === 'secondary' || termLower === 'sec') { + if (!guest.shared) return false; + return guest.primaryNode !== guest.node; + } + + // Single letter searches + if (termLower.length === 1) { + const searchText = getFullSearchableText(guest); + console.log(`Single char search "${termLower}" for ${guest.id} in: "${searchText}"`); + return searchText.includes(termLower); + } + + // Default text search + return getFullSearchableText(guest).includes(termLower); +} + +function getFullSearchableText(guest) { + // Build full searchable text + const searchableFields = [ + guest.name || '', + guest.id || '', + guest.status || '', + guest.type === 'qemu' ? 'vm virtual machine' : 'ct container', + guest.node || '', + guest.shared ? (guest.primaryNode === guest.node ? 'primary pri p' : 'secondary sec s') : '', + guest.shared ? 'shared role' : 'none' + ]; + + return searchableFields.join(' ').toLowerCase(); +} + +// Mock guests +const guests = [ + { + id: '101', + name: 'web-server-primary', + node: 'node1', + shared: true, + primaryNode: 'node1', // Primary role on current node + status: 'running', + type: 'qemu' + }, + { + id: '102', + name: 'db-primary', + node: 'node1', + shared: true, + primaryNode: 'node1', // Primary role on current node + status: 'running', + type: 'qemu' + }, + { + id: '201', + name: 'web-server-secondary', + node: 'node2', + shared: true, + primaryNode: 'node1', // Secondary role on current node (node2) + status: 'running', + type: 'qemu' + }, + { + id: '202', + name: 'db-secondary', + node: 'node2', + shared: true, + primaryNode: 'node1', // Secondary role on current node (node2) + status: 'running', + type: 'qemu' + }, + { + id: '301', + name: 'standalone-app', + node: 'node3', + shared: false, // Not shared + status: 'running', + type: 'qemu' + }, + { + id: '302', + name: 'prince-app', // Has "pri" in the name, but not primary + node: 'node3', + shared: false, + status: 'running', + type: 'qemu' + } +]; + +// Run tests for various search terms +function runTest(searchTerm) { + console.log(`\n===== TESTING SEARCH FOR: "${searchTerm}" =====`); + + const results = guests.filter(guest => { + const matches = matchesTerm(guest, searchTerm.toLowerCase(), []); + const searchableText = getFullSearchableText(guest); + + console.log(`Guest ${guest.id} (${guest.name}): ${matches ? 'MATCH' : 'NO MATCH'}`); + console.log(` Shared: ${guest.shared}, Primary node: ${guest.primaryNode}, Current node: ${guest.node}`); + console.log(` Is Primary on this node? ${guest.shared && guest.primaryNode === guest.node}`); + console.log(` Searchable text: "${searchableText}"`); + console.log(` Contains 'pri'? ${searchableText.includes('pri')}`); + console.log(` Contains 'primary'? ${searchableText.includes('primary')}`); + + return matches; + }); + + console.log(`\nRESULTS FOR "${searchTerm}":`); + console.log(` Found ${results.length} matches:`); + + if (results.length > 0) { + results.forEach(r => console.log(` - ${r.id}: ${r.name}`)); + } else { + console.log(' NO MATCHES'); + } + + // Check expectation - primary search should find primary nodes + const expectedPrimaries = guests.filter(g => g.shared && g.primaryNode === g.node); + if (searchTerm === 'pri' || searchTerm === 'primary' || searchTerm === 'role:pri' || searchTerm === 'role:primary') { + const allPrimariesFound = expectedPrimaries.every(p => results.some(r => r.id === p.id)); + const onlyPrimariesFound = results.every(r => expectedPrimaries.some(p => p.id === r.id)); + + console.log(`\nTEST RESULTS:`); + console.log(` Expected ${expectedPrimaries.length} primaries: ${expectedPrimaries.map(p => p.id).join(', ')}`); + console.log(` All primary nodes found? ${allPrimariesFound ? 'YES ✅' : 'NO ❌'}`); + console.log(` Only primary nodes found? ${onlyPrimariesFound ? 'YES ✅' : 'NO ❌'}`); + + if (!allPrimariesFound || !onlyPrimariesFound) { + console.log(`\n❌ TEST FAILED`); + } else { + console.log(`\n✅ TEST PASSED`); + } + } +} + +console.log("🔍 TESTING 'PRI' SEARCH FUNCTIONALITY"); +console.log("==================================="); + +runTest('pri'); +runTest('primary'); +runTest('role:pri'); +runTest('role:primary'); +runTest('p'); // Should match primary guests and others containing 'p' +runTest('prince'); // Should match guest 302 ("prince-app") \ No newline at end of file