import MusicAssistantCore import XCTest final class StructuredAIResponseSchemaTests: XCTestCase { func testStructuredSongProjectUpdateResponseDecodesFromJSON() throws { let json = """ { "project": { "title": "Updated Song", "idea": "A brighter chorus", "conversationMode": "auto", "languages": [{"id": "english", "name": "English"}], "dialects": [], "arabicPronunciationSettings": { "isEnabled": false, "diacritizationPolicy": "unspecified", "tanweenPolicy": "unspecified", "preservedSpellings": [] }, "genres": [{"id": "pop", "name": "Pop", "aliases": []}], "moods": [{"id": "hopeful", "name": "Hopeful"}], "emotionalArc": [], "sections": [ { "id": "chorus", "type": "chorus", "title": "Chorus", "order": 0, "lyrics": "Lift the hook higher" } ], "instruments": [ { "instrumentId": "piano", "selected": true, "autoArrangementEnabled": true, "placements": [] } ], "vocalists": [], "lyrics": { "text": "Lift the hook higher", "sectionTexts": {"chorus": "Lift the hook higher"} }, "productionDirections": [], "sunoOutput": { "lyricsText": "Lift the hook higher", "stylePrompt": "Hopeful pop with piano", "additionalFields": {}, "generatedAt": "2026-09-13T18:00:00Z" } }, "changedScopes": ["lyrics", "structure", "sunoOutput"], "notes": ["Updated requested creative fields."], "followUpQuestions": ["Should the final chorus repeat?"] } """.data(using: .utf8)! let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let response = try decoder.decode(StructuredSongProjectUpdateResponse.self, from: json) XCTAssertEqual(StructuredSongProjectUpdateResponse.schemaName, "song_project_update_response") XCTAssertEqual(response.project.title, "Updated Song") XCTAssertEqual(response.project.sections.first?.type, .chorus) XCTAssertEqual(response.project.instruments.first?.instrumentId, "piano") XCTAssertEqual(response.changedScopes, [.lyrics, .structure, .sunoOutput]) XCTAssertEqual(response.notes, ["Updated requested creative fields."]) XCTAssertEqual(response.followUpQuestions, ["Should the final chorus repeat?"]) } func testStructuredSongProjectUpdateResponseCreatesUpdateResultWhilePreservingProjectIdentityAndVersions() { let createdAt = Date(timeIntervalSince1970: 100) let updatedAt = Date(timeIntervalSince1970: 200) var existingProject = SongProject( id: "project-id", title: "Original", idea: "Original idea", createdAt: createdAt, updatedAt: createdAt ) let version = existingProject.createVersion( id: "version-id", label: "Before AI update", createdAt: createdAt ) let response = StructuredSongProjectUpdateResponse( project: SongProjectSnapshot( title: "Updated", idea: "Updated idea", genres: [GenreStyle(id: "cinematic", name: "Cinematic")], lyrics: Lyrics(text: "Updated lyrics") ), changedScopes: [.genres, .lyrics], notes: ["Proposed focused update."], followUpQuestions: ["Keep it minimal?"] ) let result = response.makeUpdateResult( updating: existingProject, updatedAt: updatedAt ) XCTAssertEqual(result.project.id, "project-id") XCTAssertEqual(result.project.createdAt, createdAt) XCTAssertEqual(result.project.updatedAt, updatedAt) XCTAssertEqual(result.project.versions, [version]) XCTAssertEqual(result.project.title, "Original") XCTAssertEqual(result.project.lyrics.text, "Updated lyrics") XCTAssertEqual(result.changedScopes, [.genres, .lyrics]) XCTAssertEqual(result.notes, ["Proposed focused update."]) XCTAssertEqual(result.followUpQuestions, ["Keep it minimal?"]) } func testStructuredResponsePreservesLockedScopesAndManualValues() { let section = SongSection(id: "verse", type: .verse, title: "Manual Verse") let manualInstrument = InstrumentTrack( instrumentId: "oud", selected: true, role: "Manual lead", autoArrangementEnabled: false, placements: [InstrumentPlacement(sectionId: "verse", direction: "Manual entry")] ) let existingProject = SongProject( title: "Original Title", idea: "Original Idea", genres: [GenreStyle(id: "arabic-pop", name: "Arabic Pop")], bpm: ManualAutoValue(mode: .manual, value: 96), key: ManualAutoValue(mode: .auto), structureMode: .manual, sections: [section], instruments: [manualInstrument], lyrics: Lyrics(text: "Locked lyrics"), userLocks: SongProjectUserLocks(scopes: [.genres, .lyrics]) ) let response = StructuredSongProjectUpdateResponse( project: SongProjectSnapshot( title: "AI Title", idea: "AI Idea", genres: [GenreStyle(id: "rock", name: "Rock")], bpm: ManualAutoValue(mode: .auto, value: 120), key: ManualAutoValue(mode: .auto, value: "A"), sections: [SongSection(id: "chorus", type: .chorus, title: "AI Chorus")], instruments: [ InstrumentTrack( instrumentId: "oud", selected: true, role: "AI lead", placements: [InstrumentPlacement(sectionId: "chorus", direction: "AI entry")] ) ], lyrics: Lyrics(text: "AI lyrics") ), changedScopes: [.genres, .lyrics, .instruments, .musicalParameters, .structure, .arrangement] ) let result = response.makeUpdateResult( updating: existingProject, updatedAt: Date(timeIntervalSince1970: 500) ) XCTAssertEqual(result.project.title, "Original Title") XCTAssertEqual(result.project.idea, "Original Idea") XCTAssertEqual(result.project.genres, existingProject.genres) XCTAssertEqual(result.project.lyrics, existingProject.lyrics) XCTAssertEqual(result.project.bpm, existingProject.bpm) XCTAssertEqual(result.project.key, ManualAutoValue(mode: .auto, value: "A")) XCTAssertEqual(result.project.sections, existingProject.sections) XCTAssertEqual(result.project.instruments, existingProject.instruments) XCTAssertEqual(result.project.userLocks, existingProject.userLocks) XCTAssertEqual(result.changedScopes, [.musicalParameters]) } }