summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-18 19:34:59 -0500
committerChristian Cleberg <[email protected]>2026-03-18 19:34:59 -0500
commit430741f0236ab21583acda8e2774d95e6e0e4ffe (patch)
treee8ef38d574939a0c47f2a92e14db60950b824151
parent796c39d8ff08829bd695bc4e7497a147640eb79a (diff)
downloadhutch-430741f0236ab21583acda8e2774d95e6e0e4ffe.tar.gz
hutch-430741f0236ab21583acda8e2774d95e6e0e4ffe.tar.bz2
hutch-430741f0236ab21583acda8e2774d95e6e0e4ffe.zip
stabilize build task identity and log cache keys
-rw-r--r--Hutch/Models/Builds.swift27
-rw-r--r--Hutch/Views/Builds/BuildDetailView.swift4
-rw-r--r--Hutch/Views/Builds/BuildDetailViewModel.swift17
-rw-r--r--HutchTests/BuildTaskTests.swift27
4 files changed, 65 insertions, 10 deletions
diff --git a/Hutch/Models/Builds.swift b/Hutch/Models/Builds.swift
index 6694c35..b0c0e8d 100644
--- a/Hutch/Models/Builds.swift
+++ b/Hutch/Models/Builds.swift
@@ -34,10 +34,33 @@ enum TaskStatus: String, Codable, Sendable {
/// A single task within a build job.
struct BuildTask: Codable, Sendable, Identifiable {
- var id: String { name }
+ private(set) var ordinal: Int?
let name: String
let status: TaskStatus
let log: BuildLog?
+
+ var id: String {
+ if let ordinal {
+ return "\(ordinal):\(name)"
+ }
+ return [name, log?.fullURL, status.rawValue]
+ .compactMap { $0 }
+ .joined(separator: "::")
+ }
+
+ var logCacheKey: String {
+ id
+ }
+
+ func withOrdinal(_ ordinal: Int) -> BuildTask {
+ var task = self
+ task.ordinal = ordinal
+ return task
+ }
+
+ private enum CodingKeys: String, CodingKey {
+ case name, status, log
+ }
}
// MARK: - Job Summary (for list view)
@@ -90,7 +113,7 @@ struct JobDetail: Codable, Sendable {
let visibility: Visibility?
let image: String?
let manifest: String?
- let tasks: [BuildTask]
+ var tasks: [BuildTask]
let log: BuildLog?
let owner: Entity
}
diff --git a/Hutch/Views/Builds/BuildDetailView.swift b/Hutch/Views/Builds/BuildDetailView.swift
index 977cd87..0479f76 100644
--- a/Hutch/Views/Builds/BuildDetailView.swift
+++ b/Hutch/Views/Builds/BuildDetailView.swift
@@ -312,13 +312,13 @@ private struct TaskLogSection: View {
var body: some View {
DisclosureGroup(isExpanded: $isExpanded) {
- if viewModel.loadingTaskLogs.contains(task.name) {
+ if viewModel.loadingTaskLogs.contains(task.logCacheKey) {
HStack {
Spacer()
ProgressView("Loading log…")
Spacer()
}
- } else if let logText = viewModel.taskLogs[task.name] {
+ } else if let logText = viewModel.taskLogs[task.logCacheKey] {
ScrollView(.horizontal, showsIndicators: false) {
Text(logText)
.font(.caption2.monospaced())
diff --git a/Hutch/Views/Builds/BuildDetailViewModel.swift b/Hutch/Views/Builds/BuildDetailViewModel.swift
index c14e47b..1768e47 100644
--- a/Hutch/Views/Builds/BuildDetailViewModel.swift
+++ b/Hutch/Views/Builds/BuildDetailViewModel.swift
@@ -104,7 +104,11 @@ final class BuildDetailViewModel {
variables: ["id": jobId],
responseType: JobDetailResponse.self
)
- job = result.job
+ var loadedJob = result.job
+ loadedJob.tasks = loadedJob.tasks.enumerated().map { index, task in
+ task.withOrdinal(index)
+ }
+ job = loadedJob
} catch {
self.error = error.localizedDescription
}
@@ -113,19 +117,20 @@ final class BuildDetailViewModel {
}
func loadTaskLog(task: BuildTask) async {
+ let cacheKey = task.logCacheKey
guard let log = task.log,
let logURL = URL(string: log.fullURL),
- !loadingTaskLogs.contains(task.name),
- taskLogs[task.name] == nil else { return }
- loadingTaskLogs.insert(task.name)
+ !loadingTaskLogs.contains(cacheKey),
+ taskLogs[cacheKey] == nil else { return }
+ loadingTaskLogs.insert(cacheKey)
do {
- taskLogs[task.name] = try await client.fetchText(url: logURL)
+ taskLogs[cacheKey] = try await client.fetchText(url: logURL)
} catch {
self.error = error.localizedDescription
}
- loadingTaskLogs.remove(task.name)
+ loadingTaskLogs.remove(cacheKey)
}
func cancelJob() async {
diff --git a/HutchTests/BuildTaskTests.swift b/HutchTests/BuildTaskTests.swift
new file mode 100644
index 0000000..2e2a1e0
--- /dev/null
+++ b/HutchTests/BuildTaskTests.swift
@@ -0,0 +1,27 @@
+import Foundation
+import Testing
+@testable import Hutch
+
+struct BuildTaskTests {
+
+ @Test
+ @MainActor
+ func duplicateTaskNamesProduceDistinctIDsAndLogCacheKeys() {
+ let firstTask = BuildTask(
+ name: "test",
+ status: .failed,
+ log: BuildLog(fullURL: "https://builds.sr.ht/job/1/task/1")
+ ).withOrdinal(0)
+
+ let secondTask = BuildTask(
+ name: "test",
+ status: .failed,
+ log: BuildLog(fullURL: "https://builds.sr.ht/job/1/task/2")
+ ).withOrdinal(1)
+
+ #expect(firstTask.id == "0:test")
+ #expect(secondTask.id == "1:test")
+ #expect(firstTask.id != secondTask.id)
+ #expect(firstTask.logCacheKey != secondTask.logCacheKey)
+ }
+}