diff --git a/Music/ContentView.swift b/Music/ContentView.swift index f1d3cc4..5264381 100644 --- a/Music/ContentView.swift +++ b/Music/ContentView.swift @@ -354,6 +354,9 @@ struct ContentView: View { SmartPlaylistBuilderSheet( editingPlaylist: smart, onSave: { name, conditions in + // Two separate writes: consistent with how mutations are handled throughout the UI. + // Partial failure (rename succeeds, conditions fail) is accepted given error + // feedback is not implemented at the UI layer. if name != smart.name { try? playlist.renameSmartPlaylist(smart, to: name) } diff --git a/MusicTests/SmartPlaylistTests.swift b/MusicTests/SmartPlaylistTests.swift index f5f63d3..956dd9c 100644 --- a/MusicTests/SmartPlaylistTests.swift +++ b/MusicTests/SmartPlaylistTests.swift @@ -267,6 +267,24 @@ struct SmartPlaylistTests { #expect((results[0].year ?? 0) < 2000) } + // Verifies that the startsWith operator treats % and _ as literal characters, + // not LIKE wildcards — confirming the ESCAPE clause in buildWhereClause works. + @Test func startsWithEscapesLIKEMetachars() throws { + // Step 1: Insert a track whose artist literally contains "%" and one whose + // artist matches the % wildcard pattern but not the literal prefix + // Step 2: Search with startsWith "A%B" — only the literal match should return + // Step 3: Verify only the literal "A%B Band" track is returned, not "AXB Band" + let db = try DatabaseService(inMemory: true) + var t1 = Track.fixture(fileURL: "/a.mp3", artist: "A%B Band") + var t2 = Track.fixture(fileURL: "/b.mp3", artist: "AXB Band") + try db.insert(&t1) + try db.insert(&t2) + let conditions = [SmartPlaylistCondition(field: .artist, op: .startsWith, value: .string("A%B"))] + let results = try db.fetchTracks(conditions: conditions, sortColumn: "title", ascending: true) + #expect(results.count == 1) + #expect(results[0].artist == "A%B Band") + } + // Updates the conditions of a structured smart playlist and verifies the updated // conditions are persisted and fetch back correctly. @Test func updateSmartPlaylistConditionsPersists() throws {