import MusicAssistantCore import XCTest final class AIServiceTests: XCTestCase { func testProviderIndependentServiceGeneratesStructuredProjectResult() async throws { let service = MockAIService() let request = SongProjectGenerationRequest( context: AIRequestContext( userInstruction: "Write a hopeful cinematic pop song.", conversation: [ AIConversationMessage(role: .user, content: "Use piano and violin.") ], localeIdentifier: "en_US" ), discussionMode: .discuss ) let result = try await service.generateSongProject(from: request) XCTAssertEqual(result.project.title, "Generated Song") XCTAssertEqual(result.project.idea, request.context.userInstruction) XCTAssertEqual(result.project.conversationMode, .discuss) XCTAssertEqual(result.notes, ["Generated by a provider-independent mock."]) XCTAssertEqual(result.followUpQuestions, ["Should the chorus be bigger?"]) } func testProviderIndependentServiceReturnsDiscussionQuestionsWithoutProjectUpdate() async throws { let service = MockAIService() let project = SongProject( title: "Discussion Project", idea: "Plan the chorus", conversationMode: .discuss ) let request = SongProjectDiscussionRequest( context: AIRequestContext(userInstruction: "Ask what is still missing."), project: project ) let result = try await service.discussSongProject(from: request) XCTAssertEqual(result.questions, ["Which vocal delivery should lead the chorus?"]) XCTAssertEqual(result.notes, ["Discussion only; no project update was proposed."]) } func testProviderIndependentServiceRevisesLyricsWithStructuredResult() async throws { let service = MockAIService() let project = SongProject(title: "Lyric Project", idea: "Improve words") let sourceLyrics = Lyrics(text: "rough draft") let request = LyricsRevisionRequest( context: AIRequestContext(userInstruction: "Make it stronger."), project: project, sourceLyrics: sourceLyrics, mode: .improve ) let result = try await service.reviseLyrics(from: request) XCTAssertEqual(result.lyrics.text, "rough draft\nMake it stronger.") XCTAssertEqual(result.notes, ["improve"]) } func testProviderIndependentServiceProposesScopedProjectUpdates() async throws { let service = MockAIService() let project = SongProject(title: "Scoped Project", idea: "Add tempo") let request = SongProjectUpdateRequest( context: AIRequestContext(userInstruction: "Set tempo manually."), project: project, allowedScopes: [.musicalParameters] ) let result = try await service.proposeProjectUpdate(from: request) XCTAssertEqual(result.project.bpm, ManualAutoValue(mode: .manual, value: 96)) XCTAssertEqual(result.changedScopes, [.musicalParameters]) XCTAssertEqual(result.notes, ["Updated only requested scopes."]) } } private struct MockAIService: AIService { func generateSongProject(from request: SongProjectGenerationRequest) async throws -> SongProjectGenerationResult { SongProjectGenerationResult( project: SongProject( title: "Generated Song", idea: request.context.userInstruction, conversationMode: request.discussionMode ), notes: ["Generated by a provider-independent mock."], followUpQuestions: ["Should the chorus be bigger?"] ) } func discussSongProject(from request: SongProjectDiscussionRequest) async throws -> SongProjectDiscussionResult { SongProjectDiscussionResult( questions: ["Which vocal delivery should lead the chorus?"], notes: ["Discussion only; no project update was proposed."] ) } func reviseLyrics(from request: LyricsRevisionRequest) async throws -> LyricsRevisionResult { LyricsRevisionResult( lyrics: Lyrics(text: "\(request.sourceLyrics.text)\n\(request.context.userInstruction)"), notes: [request.mode.rawValue] ) } func proposeProjectUpdate(from request: SongProjectUpdateRequest) async throws -> SongProjectUpdateResult { var project = request.project var changedScopes: [SongProjectUpdateScope] = [] if request.allowedScopes.contains(.musicalParameters) { project.setBPMManually(96) changedScopes.append(.musicalParameters) } return SongProjectUpdateResult( project: project, changedScopes: changedScopes, notes: ["Updated only requested scopes."] ) } }