diff --git a/Sources/agentd/ActivitySummary.swift b/Sources/agentd/ActivitySummary.swift index 148eb33..af1d2ca 100644 --- a/Sources/agentd/ActivitySummary.swift +++ b/Sources/agentd/ActivitySummary.swift @@ -194,7 +194,7 @@ struct ActivitySummary: Codable, Sendable { sourceBatchIds: sourceBatchIds.sorted(), displayIds: displayIds.sorted(), droppedCounts: dropped, - droppedReasonCounts: droppedReasonCounts.sortedByKey(), + droppedReasonCounts: droppedReasonCounts, apps: appCounters.map { key, count in ActivityAppSummary(appName: key.appName, bundleId: key.bundleId, frameCount: count) }.sorted(), @@ -261,7 +261,12 @@ struct ActivitySummary: Codable, Sendable { } private static func githubPullRequestLabel(_ raw: String) -> String? { - guard let components = URLComponents(string: raw), components.host == "github.com" else { + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + if let extractedLabel = githubPullRequestExtractedLabel(trimmed) { + return extractedLabel + } + + guard let components = URLComponents(string: trimmed), components.host == "github.com" else { return nil } let parts = components.path.split(separator: "/").map(String.init) @@ -269,6 +274,13 @@ struct ActivitySummary: Codable, Sendable { return "\(parts[0])/\(parts[1])#\(number)" } + private static func githubPullRequestExtractedLabel(_ raw: String) -> String? { + guard let match = raw.wholeMatch(of: #/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)#([0-9]+)/#) else { + return nil + } + return "\(match.1)/\(match.2)#\(match.3)" + } + private static func isoDate(_ raw: String) -> Date? { ISO8601DateFormatter().date(from: raw) } @@ -495,9 +507,3 @@ extension DropCounts { ) } } - -extension Dictionary where Key == String, Value == Int { - fileprivate func sortedByKey() -> [String: Int] { - Dictionary(uniqueKeysWithValues: sorted { $0.key < $1.key }) - } -} diff --git a/Tests/agentdTests/DiagnosticCLITests.swift b/Tests/agentdTests/DiagnosticCLITests.swift index 6e232a4..87f5fb7 100644 --- a/Tests/agentdTests/DiagnosticCLITests.swift +++ b/Tests/agentdTests/DiagnosticCLITests.swift @@ -241,6 +241,66 @@ final class DiagnosticCLITests: XCTestCase { XCTAssertTrue(markdown.contains("secret.ocrText:openai: 1")) } + func testActivitySummaryExtractsActivePullRequestLabelMetadata() async throws { + let root = try temporaryDirectory() + defer { try? FileManager.default.removeItem(at: root) } + let now = Date(timeIntervalSince1970: 21_600) + try writeBatch( + ActivitySummaryTests.batch( + id: "batch_label_only", + startedAt: Date(timeIntervalSince1970: 7_000), + endedAt: Date(timeIntervalSince1970: 7_030), + frames: [], + metadata: [ + "activePullRequest": "evalops/agentd#113", + "activePullRequest.firstSeenAt": "1970-01-01T01:56:40Z", + "activePullRequest.foregroundSeconds": "45", + ] + ), + to: root.appendingPathComponent("batch_label_only.json") + ) + + let summary = try await ActivitySummary.run( + options: ActivityOptions(sinceHours: 6, batchDirectory: root, windowLabel: "6h"), + now: now + ) + + XCTAssertEqual(summary.artifacts.map(\.label), ["evalops/agentd#113"]) + XCTAssertEqual(summary.artifacts.first?.url, "evalops/agentd#113") + XCTAssertEqual(summary.artifacts.first?.foregroundSeconds, 45) + } + + func testActivitySummaryIgnoresNonPullRequestGitHubDocumentPath() async throws { + let root = try temporaryDirectory() + defer { try? FileManager.default.removeItem(at: root) } + let now = Date(timeIntervalSince1970: 21_600) + try writeBatch( + ActivitySummaryTests.batch( + id: "batch_non_pr_url", + startedAt: Date(timeIntervalSince1970: 7_000), + endedAt: Date(timeIntervalSince1970: 7_030), + frames: [ + ActivitySummaryTests.frame( + appName: "Google Chrome", + bundleId: "com.google.Chrome", + windowTitle: "cerebro", + documentPath: "https://github.com/evalops/cerebro#123", + capturedAt: Date(timeIntervalSince1970: 7_000), + displayId: 42 + ) + ] + ), + to: root.appendingPathComponent("batch_non_pr_url.json") + ) + + let summary = try await ActivitySummary.run( + options: ActivityOptions(sinceHours: 6, batchDirectory: root, windowLabel: "6h"), + now: now + ) + + XCTAssertTrue(summary.artifacts.isEmpty) + } + func testActivitySummaryArtifactsWriteInstructionsAndResource() async throws { let batchRoot = try temporaryDirectory() let artifactRoot = try temporaryDirectory()