Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ struct ClassAIChatDependencyTests {
let isLive = withDependencies {
$0.context = .live
} operation: {
@Dependency(\.classAIChatUseCase) var useCase
return useCase is ClassAIChatUseCaseImpl
@Dependency(\.classLocalAIUseCase) var useCase
return useCase is ClassUseCaseImpl
}

#expect(isLive)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@ struct ClassAIFeedbackDependencyTests {
report: MockClassReport.figma,
isOwner: false
)
let mock = MockClassAIFeedbackUseCase { received in
let mock = FeedbackUseCaseStub { received in
#expect(received.roomID == context.roomID)
return .classInsight("generated")
}
let useCase = withDependencies {
$0.classAIFeedbackUseCase = mock
$0.classLocalAIUseCase = mock
} operation: {
@Dependency(\.classAIFeedbackUseCase) var useCase
@Dependency(\.classLocalAIUseCase) var useCase
return useCase
}
let result = try await useCase.generate(context: context)
let result = try await useCase.generateAIFeedback(context: context)
#expect(result == .classInsight("generated"))
#expect(ClassAIFeedbackError.deviceNotEligible.isUnavailable)
#expect(ClassAIFeedbackError.appleIntelligenceNotEnabled.isUnavailable)
Expand Down Expand Up @@ -130,3 +130,21 @@ struct ClassAIFeedbackDependencyTests {
}
#endif
}

private struct FeedbackUseCaseStub: ClassLocalAIInterface {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [P4] Readability

FeedbackUseCaseStub은 구조체로 선언되었으므로 final을 사용할 수 없습니다. 그러나 private 가시성을 고려할 때, 이 타입을 class로 변경하여 final로 선언하는 것을 고려할 수 있습니다. struct로 유지하는 것이 의도된 설계라면 이 코멘트는 무시해도 좋습니다.

Suggested change
private struct FeedbackUseCaseStub: ClassLocalAIInterface {
private final class FeedbackUseCaseStub: ClassLocalAIInterface {

let generate: @Sendable (ClassAIFeedbackContext) async throws -> ClassAIFeedbackResult

func startAIChat(_: UUID, _: ClassAIChatContext) async throws {
throw ClassAIFeedbackError.modelNotReady
}

func respondAIChat(_: UUID, _: String, isFinalRound _: Bool) async throws -> String {
throw ClassAIFeedbackError.modelNotReady
}

func endAIChat(_: UUID) async {}

func generateAIFeedback(context: ClassAIFeedbackContext) async throws -> ClassAIFeedbackResult {
try await generate(context)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ final class ClassAIFeedbackFoundationModelsTests: XCTestCase {
layout: .summary
)
let context = try ClassAIFeedbackContext.personal(room: room, report: report, isOwner: false)
let client = ClassAIFeedbackUseCaseDependency.liveValue
let client = ClassLocalAIUseCaseDependency.liveValue
let diagnostics = FeedbackAuditDiagnostics()
defer {
let attachment = XCTAttachment(string: diagnostics.messages.joined(separator: "\n"))
Expand All @@ -80,15 +80,15 @@ final class ClassAIFeedbackFoundationModelsTests: XCTestCase {
if #available(iOS 26.0, *) {
diagnostics.append("diagnosticSink=installed")
result = try await ClassAIFeedbackFoundationEngine.$auditDiagnostic.withValue({ diagnostics.append($0) }) {
try await client.generate(context: context)
try await client.generateAIFeedback(context: context)
}
} else {
diagnostics.append("diagnosticSink=unavailable")
result = try await client.generate(context: context)
result = try await client.generateAIFeedback(context: context)
}
#else
diagnostics.append("diagnosticSink=unavailable")
result = try await client.generate(context: context)
result = try await client.generateAIFeedback(context: context)
#endif
let attachment =
XCTAttachment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,22 @@ final class ClassAIFoundationModelsTests: XCTestCase {
throw XCTSkip("실기 Foundation Models 감사는 PICKE_CLASS_FOUNDATION_AUDIT=1에서만 실행합니다.")
}

let client = ClassAIChatUseCaseDependency.liveValue
let client = ClassLocalAIUseCaseDependency.liveValue
let conversationID = UUID()

do {
try await client.start(conversationID, ClassAIChatContext(
try await client.startAIChat(conversationID, ClassAIChatContext(
question: "뒤샹의 변기는 예술일까?",
selectedStance: "예술이 될 수 있다",
opponentStance: "예술이 아니다",
opponentPhilosopherName: "플라톤"
))
let reply = try await client.respond(
let reply = try await client.respondAIChat(
conversationID,
"평범한 물건도 새로운 의미를 부여하면 예술이 될 수 있어요.",
isFinalRound: false
)
await client.end(conversationID)
await client.endAIChat(conversationID)

let attachment = XCTAttachment(string: reply)
attachment.name = "Class AI 실제 모델 응답"
Expand All @@ -48,7 +48,7 @@ final class ClassAIFoundationModelsTests: XCTestCase {
"모델 응답에 한글 음절이 포함되어야 합니다."
)
} catch {
await client.end(conversationID)
await client.endAIChat(conversationID)
if let modelError = error as? ClassAIChatError,
modelError == .unavailable || modelError == .unsupportedLanguage
|| modelError == .deviceNotEligible || modelError == .appleIntelligenceNotEnabled
Expand Down Expand Up @@ -83,14 +83,14 @@ final class ClassAIFoundationModelsTests: XCTestCase {
"진짜? 왜 그렇게 생각해요?",
"형식뿐 아니라 보는 사람의 해석도 중요하다는 점을 정리하고 싶어요.",
]
let client = ClassAIChatUseCaseDependency.liveValue
let client = ClassLocalAIUseCaseDependency.liveValue
var replies: [String] = []
do {
try await client.start(id, context)
let first = try await client.respond(id, prompts[0], isFinalRound: false)
let second = try await client.respond(id, prompts[1], isFinalRound: false)
try await client.startAIChat(id, context)
let first = try await client.respondAIChat(id, prompts[0], isFinalRound: false)
let second = try await client.respondAIChat(id, prompts[1], isFinalRound: false)
replies = [first, second]
await client.end(id)
await client.endAIChat(id)

let prior: [ClassAIChatMessage] = [
.init(speaker: .user, text: prompts[0]),
Expand All @@ -105,10 +105,10 @@ final class ClassAIFoundationModelsTests: XCTestCase {
opponentPhilosopherName: nil,
priorMessages: prior
)
try await client.start(id, resumed)
let third = try await client.respond(id, prompts[2], isFinalRound: true)
try await client.startAIChat(id, resumed)
let third = try await client.respondAIChat(id, prompts[2], isFinalRound: true)
replies.append(third)
await client.end(id)
await client.endAIChat(id)

let limit = if #available(iOS 26.4, *) {
1440
Expand Down Expand Up @@ -142,7 +142,7 @@ final class ClassAIFoundationModelsTests: XCTestCase {
XCTAssertTrue(third.contains("해석"), "마지막 사용자 근거를 반영해야 합니다.")
XCTAssertTrue(third.contains("형식") || third.contains("기술"), "반대 입장의 근거를 반영해야 합니다.")
} catch {
await client.end(id)
await client.endAIChat(id)
throw error
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@testable import ClassDomain
import APIEndpoint

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [P2] Major

Domain 모듈의 테스트에서 APIEndpoint 모듈을 직접 가져오는 것은 아키텍처 규칙(Network는 Data 계층에서만 참조되어야 함)을 위반합니다. Domain 계층은 DataInterface 프로토콜을 통해서만 Data 계층과 상호작용해야 합니다. 테스트 시에는 DataInterface를 준수하는 Mock/Stub 객체를 사용하여 의존성을 주입해야 합니다.

APIEndpoint가 포함된 Network 또는 Data 계층의 모듈에 대한 직접적인 의존성은 Domain 계층에서 제거되어야 합니다. 이 테스트는 ClassRepositoryInterface와 같은 DomainInterface에 정의된 프로토콜을 사용하고, 해당 프로토콜의 구현체(예: ClassRepositoryImpl)가 APIEndpoint를 사용하는 형태로 이루어져야 합니다.

import ClassDomainInterface
import Foundation
import PickeNetwork
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@testable import ClassDomain
import APIEndpoint

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [P2] Major

Domain 모듈의 테스트에서 APIEndpoint 모듈을 직접 가져오는 것은 아키텍처 규칙(Network는 Data 계층에서만 참조되어야 함)을 위반합니다. Domain 계층은 DataInterface 프로토콜을 통해서만 Data 계층과 상호작용해야 합니다. 테스트 시에는 DataInterface를 준수하는 Mock/Stub 객체를 사용하여 의존성을 주입해야 합니다.

APIEndpoint가 포함된 Network 또는 Data 계층의 모듈에 대한 직접적인 의존성은 Domain 계층에서 제거되어야 합니다. 이 테스트는 ClassRepositoryInterface와 같은 DomainInterface에 정의된 프로토콜을 사용하고, 해당 프로토콜의 구현체(예: ClassRepositoryImpl)가 APIEndpoint를 사용하는 형태로 이루어져야 합니다.

import ClassDomainInterface
import Foundation
import PickeNetwork
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@testable import ClassDomain
import APIEndpoint

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [P2] Major

Domain 모듈의 테스트에서 APIEndpoint 모듈을 직접 가져오는 것은 아키텍처 규칙(Network는 Data 계층에서만 참조되어야 함)을 위반합니다. Domain 계층은 DataInterface 프로토콜을 통해서만 Data 계층과 상호작용해야 합니다. 테스트 시에는 DataInterface를 준수하는 Mock/Stub 객체를 사용하여 의존성을 주입해야 합니다.

APIEndpoint가 포함된 Network 또는 Data 계층의 모듈에 대한 직접적인 의존성은 Domain 계층에서 제거되어야 합니다. 이 테스트는 ClassRepositoryInterface와 같은 DomainInterface에 정의된 프로토콜을 사용하고, 해당 프로토콜의 구현체(예: ClassRepositoryImpl)가 APIEndpoint를 사용하는 형태로 이루어져야 합니다.

import ClassDomainInterface
import Foundation
import PickeNetwork
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@testable import ClassDomain
import APIEndpoint
import ClassDomainInterface
import Dependencies
import Foundation
Expand Down Expand Up @@ -67,13 +68,11 @@ struct ClassRecommendationTests {
@Test
func 라이브_클래스_목록은_목_저장소가_아닌_서버를_쓴다() async throws {
let json = #"{"statusCode":200,"data":[]}"#
let live = ClassLiveRepository(
api: withDependencies {
$0.networkClient = ClassRecommendationStubClient(data: Data(json.utf8))
} operation: {
ClassRepositoryImpl()
}
)
let live = withDependencies {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [P3] Minor

ClassLiveRepository가 제거되고 ClassRepositoryImpl이 직접 withDependencies 블록 내에서 인스턴스화되는 것은 TCA의 종속성 주입 패턴에 더 적합합니다. 이는 ClassRepositoryImpl이 환경 종속성(예: networkClient)을 올바르게 사용하도록 설계되었음을 나타냅니다. 잘 리팩터링되었습니다.

$0.networkClient = ClassRecommendationStubClient(data: Data(json.utf8))
} operation: {
ClassRepositoryImpl()
}

#expect(try await live.fetchMyClasses().isEmpty)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
@testable import ClassDomain
import APIEndpoint

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [P2] Major

Domain 모듈의 테스트에서 APIEndpoint 모듈을 직접 가져오는 것은 아키텍처 규칙(Network는 Data 계층에서만 참조되어야 함)을 위반합니다. Domain 계층은 DataInterface 프로토콜을 통해서만 Data 계층과 상호작용해야 합니다. 테스트 시에는 DataInterface를 준수하는 Mock/Stub 객체를 사용하여 의존성을 주입해야 합니다.

APIEndpoint가 포함된 Network 또는 Data 계층의 모듈에 대한 직접적인 의존성은 Domain 계층에서 제거되어야 합니다. 이 테스트는 ClassRepositoryInterface와 같은 DomainInterface에 정의된 프로토콜을 사용하고, 해당 프로토콜의 구현체(예: ClassRepositoryImpl)가 APIEndpoint를 사용하는 형태로 이루어져야 합니다.

import ClassDomainInterface
import Foundation
import PickeCoreUtility
import PickeNetwork
import Testing

struct ClassResultAPIContractTests {
@Test
func 결과_열람_만료와_권한_오류를_구분한다() {
#expect(ClassResultRepositoryImpl.resultError(code: "CLASS_409_RESULT_EXPIRED", message: nil) == .resultExpired)
#expect(ClassResultRepositoryImpl.resultError(code: "CLASS_403_MEMBER", message: nil) == .accessDenied)
#expect(ClassResultRepositoryImpl.feedbackError(code: "CLASS_409_RESULT_EXPIRED") == .resultExpired)
#expect(ClassResultRepositoryImpl.feedbackError(code: "CLASS_403_OWNER") == .accessDenied)
#expect(ClassResultRepositoryImpl.feedbackError(code: "CLASS_409_REPORT_RETRY") == .generationFailed)
#expect(ClassRepositoryImpl.resultError(code: "CLASS_409_RESULT_EXPIRED", message: nil) == .resultExpired)
#expect(ClassRepositoryImpl.resultError(code: "CLASS_403_MEMBER", message: nil) == .accessDenied)
#expect(ClassRepositoryImpl.feedbackError(code: "CLASS_409_RESULT_EXPIRED") == .resultExpired)
#expect(ClassRepositoryImpl.feedbackError(code: "CLASS_403_OWNER") == .accessDenied)
#expect(ClassRepositoryImpl.feedbackError(code: "CLASS_409_REPORT_RETRY") == .generationFailed)
}

@Test
func 감싼_네트워크_디코딩_오류는_재시도_대상에서_제외한다() {
let error = ClassResultRepositoryImpl.feedbackError(PickeNetworkError.decoding(.dataMissing))
let error = ClassRepositoryImpl.feedbackError(PickeNetworkError.decoding(.dataMissing))
#expect(error == .invalidResponse)
#expect(!error.isRetryable)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@testable import ClassDomain
import APIEndpoint

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [P2] Major

Domain 모듈의 테스트에서 APIEndpoint 모듈을 직접 가져오는 것은 아키텍처 규칙(Network는 Data 계층에서만 참조되어야 함)을 위반합니다. Domain 계층은 DataInterface 프로토콜을 통해서만 Data 계층과 상호작용해야 합니다. 테스트 시에는 DataInterface를 준수하는 Mock/Stub 객체를 사용하여 의존성을 주입해야 합니다.

APIEndpoint가 포함된 Network 또는 Data 계층의 모듈에 대한 직접적인 의존성은 Domain 계층에서 제거되어야 합니다. 이 테스트는 ClassRepositoryInterface와 같은 DomainInterface에 정의된 프로토콜을 사용하고, 해당 프로토콜의 구현체(예: ClassRepositoryImpl)가 APIEndpoint를 사용하는 형태로 이루어져야 합니다.

import ClassDomainInterface
import Foundation
import PickeCoreUtility
Expand All @@ -22,7 +23,7 @@ struct ClassTicketAPITests {
let error = PickeNetworkError.response(
ResponseError(httpStatus: status, code: code)
)
#expect(ClassTicketRepositoryImpl.map(error) == expected)
#expect(ClassRepositoryImpl.ticketError(error) == expected)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@testable import ClassDomain
import ClassDomainInterface
import APIEndpoint

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [P2] Major

Domain 모듈의 테스트에서 APIEndpoint 모듈을 직접 가져오는 것은 아키텍처 규칙(Network는 Data 계층에서만 참조되어야 함)을 위반합니다. Domain 계층은 DataInterface 프로토콜을 통해서만 Data 계층과 상호작용해야 합니다. 테스트 시에는 DataInterface를 준수하는 Mock/Stub 객체를 사용하여 의존성을 주입해야 합니다.

APIEndpoint가 포함된 Network 또는 Data 계층의 모듈에 대한 직접적인 의존성은 Domain 계층에서 제거되어야 합니다. 이 테스트는 ClassRepositoryInterface와 같은 DomainInterface에 정의된 프로토콜을 사용하고, 해당 프로토콜의 구현체(예: ClassRepositoryImpl)가 APIEndpoint를 사용하는 형태로 이루어져야 합니다.

import Foundation
import PickeNetwork
import Testing
Expand Down
Loading