Files
music-assistant/Tests/MusicAssistantCoreTests/SongProjectGenerationDirectorTests.swift

167 lines
7.0 KiB
Swift

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)
}
func testGenerateCompleteProjectPreservesLockedAndManualSeedValues() async throws {
let seedProject = SongProject(
id: "locked-seed",
title: "Seed",
idea: "Original",
genres: [GenreStyle(id: "arabic-pop", name: "Arabic Pop")],
bpm: ManualAutoValue(mode: .manual, value: 96),
lyrics: Lyrics(text: "User-approved lyrics"),
userLocks: SongProjectUserLocks(scopes: [.genres, .lyrics])
)
let aiService = RecordingGenerationAIService(
generationResult: SongProjectGenerationResult(
project: SongProject(
title: "Generated Title",
idea: "Generated idea",
genres: [GenreStyle(id: "rock", name: "Rock")],
bpm: ManualAutoValue(mode: .auto, value: 120),
lyrics: Lyrics(text: "AI lyrics")
)
)
)
let director = SongProjectGenerationDirector(aiService: aiService)
let result = try await director.generateCompleteProject(
fromIdea: "Generate around the locked choices",
seedProject: seedProject
)
XCTAssertEqual(result.project.title, "Generated Title")
XCTAssertEqual(result.project.idea, "Generated idea")
XCTAssertEqual(result.project.genres, seedProject.genres)
XCTAssertEqual(result.project.lyrics, seedProject.lyrics)
XCTAssertEqual(result.project.bpm, seedProject.bpm)
XCTAssertEqual(result.project.userLocks, seedProject.userLocks)
}
}
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)
}
}