const PlexMCPServer = require('../../index.js');
// Check if we should run E2E tests
const plexUrl = process.env.E2E_PLEX_URL || process.env.PLEX_URL;
const plexToken = process.env.E2E_PLEX_TOKEN || process.env.PLEX_TOKEN;
const isTestEnvironment = plexUrl?.includes('test-plex-server.com') || plexToken?.includes('test-token');
const shouldRunE2E = plexUrl && plexToken && !isTestEnvironment;
const describeE2E = shouldRunE2E ? describe : describe.skip;
describeE2E('E2E Playlist Behavior Analysis Tests', () => {
let server;
let originalEnv;
let testPlaylistId = null;
let testItemKeys = [];
let cleanupPlaylistIds = [];
beforeAll(async() => {
// Save original environment
originalEnv = { ...process.env };
console.log('π§ͺ Running detailed playlist behavior analysis against live Plex server');
console.log(`π‘ Server: ${plexUrl}`);
// Set environment for tests
process.env.PLEX_URL = plexUrl;
process.env.PLEX_TOKEN = plexToken;
server = new PlexMCPServer();
// Find some test items to work with
console.log('π Searching for test items...');
const searchResult = await server.handlePlexSearch({
query: 'music',
type: 'track',
limit: 10
});
// Extract ratingKeys from search results
const searchText = searchResult.content[0].text;
const allIds = [...searchText.matchAll(/\*\*ID: (\d+)\*\*/g)];
if (allIds.length >= 5) {
testItemKeys = allIds.slice(0, 5).map(match => match[1]);
console.log(`β
Found ${testItemKeys.length} test items:`, testItemKeys);
} else {
console.log('β οΈ Warning: Less than 5 test items found, some tests may be skipped');
testItemKeys = allIds.map(match => match[1]);
}
}, 30000);
afterAll(async() => {
// Cleanup test playlists
console.log('π§Ή Cleaning up test playlists...');
for (const playlistId of cleanupPlaylistIds) {
try {
await server.handleDeletePlaylist({ playlist_id: playlistId });
console.log(`ποΈ Deleted test playlist: ${playlistId}`);
} catch (error) {
console.log(`β οΈ Failed to cleanup playlist ${playlistId}: ${error.message}`);
}
}
// Restore original environment
process.env = originalEnv;
}, 30000);
describe('π Playlist Creation Behavior', () => {
it('should create playlist with single initial item', async() => {
if (testItemKeys.length === 0) {
console.log('βοΈ Skipping: No test items available');
return;
}
const createResult = await server.handleCreatePlaylist({
title: `E2E_Test_Single_${Date.now()}`,
type: 'audio',
item_key: testItemKeys[0]
});
console.log('π Create playlist result:', createResult.content[0].text);
// Extract playlist ID
const playlistMatch = createResult.content[0].text.match(/Playlist ID: (\w+)/);
if (playlistMatch) {
testPlaylistId = playlistMatch[1];
cleanupPlaylistIds.push(testPlaylistId);
console.log(`β
Created test playlist: ${testPlaylistId}`);
}
expect(createResult.content[0].text).toMatch(/Successfully created playlist|Playlist ID:/);
}, 15000);
it('should verify initial playlist contents', async() => {
if (!testPlaylistId) {
console.log('βοΈ Skipping: No test playlist created');
return;
}
const browseResult = await server.handleBrowsePlaylist({
playlist_id: testPlaylistId
});
console.log('π Initial playlist contents:', browseResult.content[0].text);
expect(browseResult.content[0].text).toMatch(/Items: \d+/);
}, 10000);
});
describe('β Add Operations Analysis', () => {
it('should test adding SINGLE item to existing playlist', async() => {
if (!testPlaylistId || testItemKeys.length < 2) {
console.log('βοΈ Skipping: Prerequisites not met');
return;
}
console.log(`π΅ Adding single item ${testItemKeys[1]} to playlist ${testPlaylistId}`);
const addResult = await server.handleAddToPlaylist({
playlist_id: testPlaylistId,
item_keys: [testItemKeys[1]]
});
console.log('π Single add result:', addResult.content[0].text);
// Verify playlist after addition
const browseAfter = await server.handleBrowsePlaylist({
playlist_id: testPlaylistId
});
console.log('π Playlist after single add:', browseAfter.content[0].text);
expect(addResult.content[0].text).toMatch(/Attempted to add: 1 item/);
}, 15000);
it('should test adding MULTIPLE items at once (the failing case)', async() => {
if (!testPlaylistId || testItemKeys.length < 5) {
console.log('βοΈ Skipping: Prerequisites not met');
return;
}
console.log(`π΅ Adding multiple items [${testItemKeys[2]}, ${testItemKeys[3]}, ${testItemKeys[4]}] to playlist ${testPlaylistId}`);
const addResult = await server.handleAddToPlaylist({
playlist_id: testPlaylistId,
item_keys: [testItemKeys[2], testItemKeys[3], testItemKeys[4]]
});
console.log('π Multiple add result:', addResult.content[0].text);
// Verify playlist after addition
const browseAfter = await server.handleBrowsePlaylist({
playlist_id: testPlaylistId
});
console.log('π Playlist after multiple add:', browseAfter.content[0].text);
expect(addResult.content[0].text).toMatch(/Attempted to add: 3 item/);
}, 15000);
it('should test adding duplicate items (should be ignored)', async() => {
if (!testPlaylistId || testItemKeys.length < 2) {
console.log('βοΈ Skipping: Prerequisites not met');
return;
}
console.log(`π Adding duplicate item ${testItemKeys[0]} to playlist ${testPlaylistId}`);
const addResult = await server.handleAddToPlaylist({
playlist_id: testPlaylistId,
item_keys: [testItemKeys[0]]
});
console.log('π Duplicate add result:', addResult.content[0].text);
expect(addResult.content[0].text).toMatch(/Attempted to add: 1 item/);
}, 15000);
});
describe('β Remove Operations Analysis (CRITICAL)', () => {
it('should document current playlist state before removal', async() => {
if (!testPlaylistId) {
console.log('βοΈ Skipping: No test playlist available');
return;
}
const browseResult = await server.handleBrowsePlaylist({
playlist_id: testPlaylistId
});
console.log('π BEFORE REMOVAL - Playlist state:', browseResult.content[0].text);
// Extract item count for comparison
const itemCountMatch = browseResult.content[0].text.match(/Items: (\d+)/);
if (itemCountMatch) {
console.log(`π Items before removal: ${itemCountMatch[1]}`);
}
}, 10000);
it('should test removing SINGLE item (the dangerous operation)', async() => {
if (!testPlaylistId || testItemKeys.length < 1) {
console.log('βοΈ Skipping: Prerequisites not met');
return;
}
console.log(`ποΈ CRITICAL TEST: Removing single item ${testItemKeys[0]} from playlist ${testPlaylistId}`);
console.log('β οΈ WARNING: This may remove ALL playlist contents due to the bug!');
const removeResult = await server.handleRemoveFromPlaylist({
playlist_id: testPlaylistId,
item_keys: [testItemKeys[0]]
});
console.log('π Remove result:', removeResult.content[0].text);
// Check playlist state after removal
const browseAfter = await server.handleBrowsePlaylist({
playlist_id: testPlaylistId
});
console.log('π AFTER REMOVAL - Playlist state:', browseAfter.content[0].text);
// Extract item count to see what actually happened
const itemCountMatch = browseAfter.content[0].text.match(/Items: (\d+)/);
if (itemCountMatch) {
console.log(`π Items after removal: ${itemCountMatch[1]}`);
if (itemCountMatch[1] === '0') {
console.log('π¨ CRITICAL BUG CONFIRMED: Remove operation emptied entire playlist!');
}
}
expect(removeResult.content[0].text).toMatch(/Attempted to remove: 1 item/);
}, 15000);
});
describe('π§ͺ API Behavior Patterns', () => {
it('should create fresh playlist for pattern testing', async() => {
if (testItemKeys.length < 3) {
console.log('βοΈ Skipping: Not enough test items');
return;
}
const createResult = await server.handleCreatePlaylist({
title: `E2E_Pattern_Test_${Date.now()}`,
type: 'audio',
item_key: testItemKeys[0]
});
console.log('π Pattern test playlist result:', createResult.content[0].text);
const playlistMatch = createResult.content[0].text.match(/Playlist ID: (\w+)/);
if (playlistMatch) {
const patternTestPlaylistId = playlistMatch[1];
cleanupPlaylistIds.push(patternTestPlaylistId);
// Test the exact sequence: create -> add single -> add multiple -> remove
console.log('\n㪠TESTING EXACT SEQUENCE PATTERN:');
// Step 1: Add single item
console.log('1οΈβ£ Adding single item...');
const singleAdd = await server.handleAddToPlaylist({
playlist_id: patternTestPlaylistId,
item_keys: [testItemKeys[1]]
});
console.log(' Result:', singleAdd.content[0].text);
// Step 2: Check state
const afterSingle = await server.handleBrowsePlaylist({
playlist_id: patternTestPlaylistId
});
console.log(' State after single add:', afterSingle.content[0].text.match(/Items: (\d+)/)?.[1] || 'unknown');
// Step 3: Add multiple items
console.log('2οΈβ£ Adding multiple items...');
const multipleAdd = await server.handleAddToPlaylist({
playlist_id: patternTestPlaylistId,
item_keys: [testItemKeys[2]]
});
console.log(' Result:', multipleAdd.content[0].text);
// Step 4: Check final state
const finalState = await server.handleBrowsePlaylist({
playlist_id: patternTestPlaylistId
});
console.log(' Final state:', finalState.content[0].text.match(/Items: (\d+)/)?.[1] || 'unknown');
}
expect(createResult.content[0].text).toMatch(/Successfully created playlist/);
}, 20000);
});
describe('π Response Message Analysis', () => {
it('should analyze response accuracy patterns', async() => {
console.log('\nπ RESPONSE MESSAGE ANALYSIS:');
console.log('This test documents what the API actually returns vs what messages we show');
// This test is mainly for documentation - it will pass regardless
// but logs the exact patterns we're seeing
console.log('β
Test completed - check logs above for response patterns');
expect(true).toBe(true);
});
});
describe('π§Ή Comprehensive Cleanup Verification', () => {
it('should verify all test playlists can be properly deleted', async() => {
console.log('\nποΈ TESTING PLAYLIST DELETION:');
if (cleanupPlaylistIds.length === 0) {
console.log('β
No playlists to clean up - all tests cleaned up after themselves');
expect(true).toBe(true);
return;
}
let deletionResults = [];
for (const playlistId of cleanupPlaylistIds) {
try {
const deleteResult = await server.handleDeletePlaylist({
playlist_id: playlistId
});
deletionResults.push({
id: playlistId,
success: true,
message: deleteResult.content[0].text
});
console.log(`β
Deleted ${playlistId}: ${deleteResult.content[0].text}`);
} catch (error) {
deletionResults.push({
id: playlistId,
success: false,
message: error.message
});
console.log(`β Failed to delete ${playlistId}: ${error.message}`);
}
}
console.log('π Deletion summary:', deletionResults);
// Clear the cleanup array since we've processed them
cleanupPlaylistIds = [];
expect(deletionResults.length).toBeGreaterThan(0);
}, 30000);
});
});