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
12 changes: 10 additions & 2 deletions Sources/Zero/Services/ContainerOrchestrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,18 @@ class ContainerOrchestrator {

// 3-1. Git 설치 (Alpine 기반 이미지에 git이 없을 경우)
if image.contains("alpine") {
_ = try? dockerService.executeShell(container: containerName, script: "apk add --no-cache git")
do {
_ = try dockerService.executeShell(container: containerName, script: "apk add --no-cache git")
} catch {
AppLogStore.shared.append("ContainerOrchestrator git install failed (apk): \(error.localizedDescription)")
}
} else if image.contains("openjdk") || image.contains("temurin") || image.contains("corretto") {
// JDK 이미지들은 보통 Debian/Ubuntu 기반이므로 apt 사용
_ = try? dockerService.executeShell(container: containerName, script: "apt-get update && apt-get install -y git")
do {
_ = try dockerService.executeShell(container: containerName, script: "apt-get update && apt-get install -y git")
} catch {
AppLogStore.shared.append("ContainerOrchestrator git install failed (apt): \(error.localizedDescription)")
}
}

// 3. Git Clone (토큰 주입)
Expand Down
43 changes: 43 additions & 0 deletions Tests/ZeroTests/ContainerOrchestratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class MockDockerService: DockerServiceProtocol {
var lastContainerName: String?
var lastImageName: String?
var executedScripts: [String] = []
var shellErrorsByCommandSubstring: [String: Error] = [:]

// 호환성을 위한 계산 속성
var executedScript: String? {
Expand All @@ -20,11 +21,17 @@ class MockDockerService: DockerServiceProtocol {
}

func executeShell(container: String, script: String) throws -> String {
if let match = shellErrorsByCommandSubstring.first(where: { script.contains($0.key) }) {
throw match.value
}
executedScripts.append(script)
return "mock shell output"
}

func executeShellStreaming(container: String, script: String, onOutput: @escaping (String) -> Void) throws -> String {
if let match = shellErrorsByCommandSubstring.first(where: { script.contains($0.key) }) {
throw match.value
}
executedScripts.append(script)
let output = "mock shell output"
onOutput(output)
Expand Down Expand Up @@ -57,9 +64,11 @@ final class ContainerOrchestratorTests: XCTestCase {
override func setUp() {
super.setUp()
testStoreURL = FileManager.default.temporaryDirectory.appendingPathComponent("orchestrator_test.json")
AppLogStore.shared.clear()
}

override func tearDown() {
AppLogStore.shared.clear()
try? FileManager.default.removeItem(at: testStoreURL)
super.tearDown()
}
Expand Down Expand Up @@ -100,4 +109,38 @@ final class ContainerOrchestratorTests: XCTestCase {
let sessions = try sessionManager.loadSessions()
XCTAssertEqual(sessions.count, 1)
}

func testStartSessionLogsGitInstallFailureAndContinues() async throws {
// Given
let mockDocker = MockDockerService()
mockDocker.shellErrorsByCommandSubstring["apk add --no-cache git"] = NSError(
domain: "docker",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "apk failed: temporary network error"]
)
let sessionManager = SessionManager(storeURL: testStoreURL)
let orchestrator = ContainerOrchestrator(
dockerService: mockDocker,
sessionManager: sessionManager
)

let repo = Repository(
id: 1,
name: "test-repo",
fullName: "user/test-repo",
isPrivate: false,
htmlURL: URL(string: "https://github.com/user/test-repo")!,
cloneURL: URL(string: "https://github.com/user/test-repo.git")!
)

// When
let session = try await orchestrator.startSession(repo: repo, token: "ghp_test_token")

// Then
XCTAssertEqual(session.repoURL, repo.cloneURL)
let logs = AppLogStore.shared.recentEntries()
XCTAssertTrue(logs.contains { entry in
entry.contains("ContainerOrchestrator git install failed") && entry.contains("apk failed")
})
}
}