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
6 changes: 5 additions & 1 deletion apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2879,14 +2879,18 @@ struct OperatorLaneHeaderReadoutView: View {
.foregroundStyle(PanelPalette.primaryText(colorScheme).opacity(0.94))
.lineLimit(1)
.truncationMode(.tail)
.layoutPriority(1)
.frame(maxWidth: .infinity, alignment: .leading)

Spacer(minLength: 8)

if let project = panelTrimmed(project) {
Text(project)
.font(PanelFont.laneDetail)
.foregroundStyle(PanelPalette.secondaryText(colorScheme).opacity(0.82))
.lineLimit(1)
.truncationMode(.middle)
.fixedSize(horizontal: true, vertical: false)
.frame(alignment: .trailing)
.help(project)
}
}
Expand Down
33 changes: 6 additions & 27 deletions apps/decodex-app/Sources/DecodexApp/AccountStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ final class AccountStore: ObservableObject {
private let bridge = DecodexAppBridge()
private var automaticRefreshTask: Task<Void, Never>?
private var operatorSnapshotStreamTask: Task<Void, Never>?
private var pendingRunActivity: OperatorRunActivitySnapshot?
private var liveRunActivity: OperatorRunActivitySnapshot?

deinit {
automaticRefreshTask?.cancel()
Expand Down Expand Up @@ -155,7 +155,7 @@ final class AccountStore: ObservableObject {
do {
try await connectOperatorSnapshotStream()
} catch {
pendingRunActivity = nil
liveRunActivity = nil
}

do {
Expand Down Expand Up @@ -204,7 +204,7 @@ final class AccountStore: ObservableObject {
return try JSONDecoder().decode(OperatorDashboardSocketEvent.self, from: data)
}

private func applyOperatorDashboardEvent(_ event: OperatorDashboardSocketEvent) {
func applyOperatorDashboardEvent(_ event: OperatorDashboardSocketEvent) {
guard let payload = event.payload else {
return
}
Expand All @@ -215,16 +215,8 @@ final class AccountStore: ObservableObject {
return
}

let snapshotPublishedAt = payload.snapshotPublishedAt ?? Date()
if let pendingRunActivity,
pendingRunActivity.shouldOverlay(snapshotPublishedAt: snapshotPublishedAt)
{
operatorSnapshot = pendingRunActivity.merging(into: snapshot)
} else {
operatorSnapshot = snapshot
pendingRunActivity = nil
}
operatorSnapshotUpdatedAt = snapshotPublishedAt
operatorSnapshot = liveRunActivity?.merging(into: snapshot) ?? snapshot
operatorSnapshotUpdatedAt = payload.snapshotPublishedAt ?? Date()
case "runActivity":
guard let activeRuns = payload.activeRuns else {
return
Expand All @@ -235,20 +227,7 @@ final class AccountStore: ObservableObject {
activeRunsComplete: payload.activeRunsComplete ?? true,
emittedAt: payload.emittedAt ?? Date()
)
if let operatorSnapshotUpdatedAt,
activity.shouldOverlay(snapshotPublishedAt: operatorSnapshotUpdatedAt) == false
{
pendingRunActivity = nil

return
}
guard activity.shouldApply(to: operatorSnapshot) else {
pendingRunActivity = nil

return
}

pendingRunActivity = activity
liveRunActivity = activity
if let operatorSnapshot {
self.operatorSnapshot = activity.merging(into: operatorSnapshot)
}
Expand Down
19 changes: 0 additions & 19 deletions apps/decodex-app/Sources/DecodexApp/OperatorSnapshotModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -589,28 +589,9 @@ struct OperatorRunActivitySnapshot: Sendable {
let activeRunsComplete: Bool
let emittedAt: Date

func shouldOverlay(snapshotPublishedAt: Date?) -> Bool {
guard let snapshotPublishedAt else {
return true
}

return emittedAt > snapshotPublishedAt
}

func merging(into snapshot: OperatorSnapshotResponse) -> OperatorSnapshotResponse {
snapshot.mergingRunActivity(activeRuns, activeRunsComplete: activeRunsComplete)
}

func shouldApply(to snapshot: OperatorSnapshotResponse?) -> Bool {
guard let snapshot else {
return true
}
if activeRunsComplete, activeRuns.isEmpty, snapshot.activeRuns.isEmpty == false {
return false
}

return true
}
}

struct OperatorChildAgentActivity: Decodable, Sendable {
Expand Down
215 changes: 152 additions & 63 deletions apps/decodex-app/Tests/DecodexAppTests/AccountModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -202,53 +202,56 @@ final class AccountModelTests: XCTestCase {
XCTAssertTrue(snapshot.activeRuns(for: poolOnlyAccount).isEmpty)
}

func testOperatorRunActivityOverlayDoesNotReplaceNewerSnapshot() throws {
@MainActor
func testOperatorRunActivityUsesStreamOrderOverSnapshotTimestamp() throws {
let account = makeAccount(
status: "available",
email: "copy@example.com",
accountFingerprint: "...123456"
)
let snapshotPayload = """
{
"active_runs": [
{
"run_id": "run-new",
"issue_identifier": "XY-672",
"account": {
"email": "copy@example.com",
"account_fingerprint": "...123456"
}
}
]
}
""".data(using: .utf8)!
let activityPayload = """
{
"activeRuns": [
{
"run_id": "run-old",
"issue_identifier": "PUB-1147",
"account": {
"email": "copy@example.com",
"account_fingerprint": "...123456"
}
}
]
}
""".data(using: .utf8)!

let snapshot = try JSONDecoder().decode(OperatorSnapshotResponse.self, from: snapshotPayload)
let activity = try JSONDecoder()
.decode(OperatorDashboardSocketPayload.self, from: activityPayload)
.activeRuns ?? []
let overlay = OperatorRunActivitySnapshot(
activeRuns: activity,
activeRunsComplete: true,
emittedAt: Date(timeIntervalSince1970: 10)
)

XCTAssertFalse(overlay.shouldOverlay(snapshotPublishedAt: Date(timeIntervalSince1970: 20)))
XCTAssertEqual(snapshot.activeRuns(for: account).map(\.runID), ["run-new"])
let store = AccountStore()

try store.applyOperatorDashboardEvent(dashboardEvent(
type: "snapshot",
payload: """
{
"snapshotPublishedAtUnixEpoch": 20,
"snapshot": {
"active_runs": [
{
"run_id": "run-new",
"issue_identifier": "XY-672",
"account": {
"email": "copy@example.com",
"account_fingerprint": "...123456"
}
}
]
}
}
"""
))
try store.applyOperatorDashboardEvent(dashboardEvent(
type: "runActivity",
payload: """
{
"emittedAtUnixEpoch": 10,
"activeRunsComplete": true,
"activeRuns": [
{
"run_id": "run-old",
"issue_identifier": "PUB-1147",
"account": {
"email": "copy@example.com",
"account_fingerprint": "...123456"
}
}
]
}
"""
))

XCTAssertEqual(store.operatorSnapshot?.activeRuns(for: account).map(\.runID), ["run-old"])
}

func testPartialRunActivityPreservesSnapshotActiveRuns() throws {
Expand Down Expand Up @@ -307,7 +310,6 @@ final class AccountModelTests: XCTestCase {
)
let merged = overlay.merging(into: snapshot)

XCTAssertTrue(overlay.shouldOverlay(snapshotPublishedAt: Date(timeIntervalSince1970: 20)))
XCTAssertEqual(merged.activeRuns.map(\.runID), ["run-689", "run-690"])
XCTAssertEqual(merged.activeRuns(for: account).map(\.runID), ["run-689", "run-690"])
}
Expand Down Expand Up @@ -401,7 +403,6 @@ final class AccountModelTests: XCTestCase {
)
let merged = overlay.merging(into: snapshot)

XCTAssertTrue(overlay.shouldOverlay(snapshotPublishedAt: Date(timeIntervalSince1970: 20)))
XCTAssertEqual(merged.activeRuns.map(\.runID), ["run-690"])
XCTAssertEqual(merged.activeRuns(for: account).map(\.runID), ["run-690"])
}
Expand Down Expand Up @@ -434,30 +435,104 @@ final class AccountModelTests: XCTestCase {
)
let merged = overlay.merging(into: snapshot)

XCTAssertTrue(overlay.shouldOverlay(snapshotPublishedAt: Date(timeIntervalSince1970: 20)))
XCTAssertTrue(merged.activeRuns(for: account).isEmpty)
}

func testCompleteEmptyRunActivityWaitsForSnapshotBeforeClearingVisibleRuns() throws {
let snapshotPayload = """
{
"active_runs": [
{
"run_id": "run-old",
"issue_identifier": "XY-672"
}
]
}
""".data(using: .utf8)!
let snapshot = try JSONDecoder().decode(OperatorSnapshotResponse.self, from: snapshotPayload)
let overlay = OperatorRunActivitySnapshot(
activeRuns: [],
activeRunsComplete: true,
emittedAt: Date(timeIntervalSince1970: 30)
@MainActor
func testLiveRunActivitySurvivesNewerEmptySnapshot() throws {
let account = makeAccount(
status: "available",
email: "copy@example.com",
accountFingerprint: "...123456"
)
let store = AccountStore()

try store.applyOperatorDashboardEvent(dashboardEvent(
type: "snapshot",
payload: """
{
"snapshotPublishedAtUnixEpoch": 20,
"snapshot": {
"active_runs": []
}
}
"""
))
try store.applyOperatorDashboardEvent(dashboardEvent(
type: "runActivity",
payload: """
{
"emittedAtUnixEpoch": 30,
"activeRunsComplete": true,
"activeRuns": [
{
"run_id": "run-live",
"issue_identifier": "XY-672",
"account": {
"email": "copy@example.com",
"account_fingerprint": "...123456"
}
}
]
}
"""
))
try store.applyOperatorDashboardEvent(dashboardEvent(
type: "snapshot",
payload: """
{
"snapshotPublishedAtUnixEpoch": 40,
"snapshot": {
"active_runs": []
}
}
"""
))

XCTAssertEqual(store.operatorSnapshot?.activeRuns(for: account).map(\.runID), ["run-live"])
}

XCTAssertFalse(overlay.shouldApply(to: snapshot))
XCTAssertTrue(overlay.shouldApply(to: nil))
@MainActor
func testCompleteEmptyRunActivityClearsLiveRuns() throws {
let account = makeAccount(
status: "available",
email: "copy@example.com",
accountFingerprint: "...123456"
)
let store = AccountStore()

try store.applyOperatorDashboardEvent(dashboardEvent(
type: "snapshot",
payload: """
{
"snapshotPublishedAtUnixEpoch": 20,
"snapshot": {
"active_runs": [
{
"run_id": "run-live",
"issue_identifier": "XY-672",
"account": {
"email": "copy@example.com",
"account_fingerprint": "...123456"
}
}
]
}
}
"""
))
try store.applyOperatorDashboardEvent(dashboardEvent(
type: "runActivity",
payload: """
{
"emittedAtUnixEpoch": 30,
"activeRunsComplete": true,
"activeRuns": []
}
"""
))

XCTAssertTrue(store.operatorSnapshot?.activeRuns(for: account).isEmpty ?? false)
}

func testOperatorSnapshotWarningSummaryUsesRawWarningToken() throws {
Expand All @@ -472,6 +547,20 @@ final class AccountModelTests: XCTestCase {
XCTAssertEqual(snapshot.warningSummary, "external_observer_status_skipped")
}

private func dashboardEvent(
type: String,
payload: String
) throws -> OperatorDashboardSocketEvent {
let data = """
{
"type": "\(type)",
"payload": \(payload)
}
""".data(using: .utf8)!

return try JSONDecoder().decode(OperatorDashboardSocketEvent.self, from: data)
}

private func makeAccount(
status: String,
email: String = "copy@example.com",
Expand Down