import MusicAssistantCore import XCTest final class SongProjectGenerationDirectorTests: XCTestCase { func testGenerateCompleteProjectBuildsGenerationRequestFromTrimmedIdea() async throws { let aiService = RecordingGenerationAIService( generationResult: SongProjectGenerationResult( project: SongProject( title: "Generated Song", idea: "Hopeful cinematic pop", genres: [GenreStyle(id: "cinematic-pop", name: "Cinematic Pop")] ), notes: ["Completed from idea."] ) ) let director = SongProjectGenerationDirector(aiService: aiService) let conversation = [ AIConversationMessage(role: .user, content: "Use piano.") ] let result = try await director.generateCompleteProject( fromIdea: " Hopeful cinematic pop ", discussionMode: .discuss, conversation: conversation, localeIdentifier: "en_US", generatedAt: Date(timeIntervalSince1970: 200) ) let recordedRequest = await aiService.recordedGenerationRequests.first XCTAssertEqual(recordedRequest?.context.userInstruction, "Hopeful cinematic pop") XCTAssertEqual(recordedRequest?.context.conversation, conversation) XCTAssertEqual(recordedRequest?.context.localeIdentifier, "en_US") XCTAssertEqual(recordedRequest?.discussionMode, .discuss) XCTAssertNil(recordedRequest?.seedProject) XCTAssertEqual(result.project.conversationMode, .discuss) XCTAssertEqual(result.project.updatedAt, Date(timeIntervalSince1970: 200)) XCTAssertEqual(result.notes, ["Completed from idea."]) } func testGenerateCompleteProjectPreservesSeedProjectIdentityAndVersions() async throws { let createdAt = Date(timeIntervalSince1970: 10) let generatedAt = Date(timeIntervalSince1970: 300) var seedProject = SongProject( id: "seed-project", title: "Draft", idea: "Original idea", createdAt: createdAt, updatedAt: createdAt ) let version = seedProject.createVersion( id: "seed-version", label: "Before generation", createdAt: createdAt ) let aiService = RecordingGenerationAIService( generationResult: SongProjectGenerationResult( project: SongProject( id: "generated-id", title: "Generated Complete Project", idea: "", sections: [ SongSection(id: "verse", type: .verse, title: "Verse", order: 0) ], lyrics: Lyrics(text: "Generated lyrics") ) ) ) let director = SongProjectGenerationDirector(aiService: aiService) let result = try await director.generateCompleteProject( fromIdea: "Build from this idea", seedProject: seedProject, generatedAt: generatedAt ) let recordedRequest = await aiService.recordedGenerationRequests.first XCTAssertEqual(recordedRequest?.seedProject, seedProject) XCTAssertEqual(result.project.id, "seed-project") XCTAssertEqual(result.project.createdAt, createdAt) XCTAssertEqual(result.project.updatedAt, generatedAt) XCTAssertEqual(result.project.versions, [version]) XCTAssertEqual(result.project.title, "Generated Complete Project") XCTAssertEqual(result.project.idea, "Build from this idea") XCTAssertEqual(result.project.sections.first?.id, "verse") XCTAssertEqual(result.project.lyrics.text, "Generated lyrics") } func testGenerateCompleteProjectRejectsBlankIdeaBeforeCallingAI() async { let aiService = RecordingGenerationAIService( generationResult: SongProjectGenerationResult( project: SongProject(title: "Unused", idea: "Unused") ) ) let director = SongProjectGenerationDirector(aiService: aiService) do { _ = try await director.generateCompleteProject(fromIdea: " \n ") XCTFail("Expected blank idea to throw.") } catch let error as SongProjectGenerationDirectorError { XCTAssertEqual(error, .emptyIdea) } catch { XCTFail("Expected SongProjectGenerationDirectorError.") } let requestCount = await aiService.recordedGenerationRequests.count XCTAssertEqual(requestCount, 0) } } private actor RecordingGenerationAIService: AIService { private(set) var recordedGenerationRequests: [SongProjectGenerationRequest] = [] private let generationResult: SongProjectGenerationResult init(generationResult: SongProjectGenerationResult) { self.generationResult = generationResult } func generateSongProject(from request: SongProjectGenerationRequest) async throws -> SongProjectGenerationResult { recordedGenerationRequests.append(request) return generationResult } func reviseLyrics(from request: LyricsRevisionRequest) async throws -> LyricsRevisionResult { LyricsRevisionResult(lyrics: request.sourceLyrics) } func proposeProjectUpdate(from request: SongProjectUpdateRequest) async throws -> SongProjectUpdateResult { SongProjectUpdateResult(project: request.project) } }