Enforce user locks for AI updates
This commit is contained in:
@@ -188,6 +188,29 @@ final class AutoSongProjectDecisionsDirectorTests: XCTestCase {
|
||||
XCTAssertEqual(result.project.productionDirections, project.productionDirections)
|
||||
XCTAssertEqual(result.changedScopes, [SongProjectUpdateScope.structure])
|
||||
}
|
||||
|
||||
func testAutomaticDecisionsDoNotCallAIForLockedAutomaticScopes() async throws {
|
||||
let project = SongProject(
|
||||
title: "Locked Auto Project",
|
||||
idea: "Do not update the tempo",
|
||||
bpm: ManualAutoValue(mode: .auto),
|
||||
key: ManualAutoValue(mode: .manual, value: "C"),
|
||||
scale: ManualAutoValue(mode: .manual, value: "minor"),
|
||||
maqam: ManualAutoValue(mode: .manual, value: "Rast"),
|
||||
structureMode: .manual,
|
||||
productionMode: .manual,
|
||||
userLocks: SongProjectUserLocks(scopes: [.musicalParameters])
|
||||
)
|
||||
let aiService = RecordingAutoDecisionsAIService(result: SongProjectUpdateResult(project: project))
|
||||
let director = AutoSongProjectDecisionsDirector(aiService: aiService)
|
||||
|
||||
let result = try await director.applyAutomaticDecisions(to: project)
|
||||
let requestCount = await aiService.recordedRequests.count
|
||||
|
||||
XCTAssertEqual(requestCount, 0)
|
||||
XCTAssertEqual(result.project, project)
|
||||
XCTAssertTrue(result.changedScopes.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
private actor RecordingAutoDecisionsAIService: AIService {
|
||||
|
||||
@@ -137,6 +137,30 @@ final class ExistingLyricsRevisionDirectorTests: XCTestCase {
|
||||
let requestCount = await aiService.recordedRevisionRequests.count
|
||||
XCTAssertEqual(requestCount, 0)
|
||||
}
|
||||
|
||||
func testReviseExistingLyricsDoesNotCallAIWhenLyricsAreLocked() async throws {
|
||||
let aiService = RecordingLyricsAIService(
|
||||
revisionResult: LyricsRevisionResult(lyrics: Lyrics(text: "Unused"))
|
||||
)
|
||||
let director = ExistingLyricsRevisionDirector(aiService: aiService)
|
||||
let project = SongProject(
|
||||
title: "Locked Lyrics",
|
||||
idea: "Keep the words",
|
||||
lyrics: Lyrics(text: "User-approved lyrics"),
|
||||
userLocks: SongProjectUserLocks(scopes: [.lyrics])
|
||||
)
|
||||
|
||||
let result = try await director.reviseExistingLyrics(
|
||||
in: project,
|
||||
instruction: "Improve the chorus.",
|
||||
mode: .improve
|
||||
)
|
||||
let requestCount = await aiService.recordedRevisionRequests.count
|
||||
|
||||
XCTAssertEqual(requestCount, 0)
|
||||
XCTAssertEqual(result.project, project)
|
||||
XCTAssertTrue(result.changedScopes.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
private actor RecordingLyricsAIService: AIService {
|
||||
|
||||
@@ -105,6 +105,42 @@ final class SongProjectGenerationDirectorTests: XCTestCase {
|
||||
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 {
|
||||
|
||||
@@ -105,10 +105,69 @@ final class StructuredAIResponseSchemaTests: XCTestCase {
|
||||
XCTAssertEqual(result.project.createdAt, createdAt)
|
||||
XCTAssertEqual(result.project.updatedAt, updatedAt)
|
||||
XCTAssertEqual(result.project.versions, [version])
|
||||
XCTAssertEqual(result.project.title, "Updated")
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user