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
8 changes: 8 additions & 0 deletions Packages/CrowCore/Sources/CrowCore/Models/Session.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ public struct Session: Identifiable, Codable, Sendable {
public var ticketTitle: String?
public var ticketNumber: Int?
public var provider: Provider?
// Code-source provider, distinct from the task-source `provider`. Lets a
// Corveil-tasked session use a GitHub or GitLab `CodeBackend` (ADR 0005,
// CROW-414). `nil` means "follow `provider`"; callers resolve with
// `session.codeProvider ?? session.provider`.
public var codeProvider: Provider?
public var createdAt: Date
public var updatedAt: Date
// Whether a review-kind session has had its initial `/crow-review-pr`
Expand Down Expand Up @@ -43,6 +48,7 @@ public struct Session: Identifiable, Codable, Sendable {
ticketTitle: String? = nil,
ticketNumber: Int? = nil,
provider: Provider? = nil,
codeProvider: Provider? = nil,
createdAt: Date = Date(),
updatedAt: Date = Date(),
reviewPromptDispatched: Bool = false,
Expand All @@ -58,6 +64,7 @@ public struct Session: Identifiable, Codable, Sendable {
self.ticketTitle = ticketTitle
self.ticketNumber = ticketNumber
self.provider = provider
self.codeProvider = codeProvider
self.createdAt = createdAt
self.updatedAt = updatedAt
self.reviewPromptDispatched = reviewPromptDispatched
Expand Down Expand Up @@ -93,6 +100,7 @@ public struct Session: Identifiable, Codable, Sendable {
ticketTitle = try container.decodeIfPresent(String.self, forKey: .ticketTitle)
ticketNumber = try container.decodeIfPresent(Int.self, forKey: .ticketNumber)
provider = try container.decodeIfPresent(Provider.self, forKey: .provider)
codeProvider = try container.decodeIfPresent(Provider.self, forKey: .codeProvider)
createdAt = try container.decode(Date.self, forKey: .createdAt)
updatedAt = try container.decode(Date.self, forKey: .updatedAt)
reviewPromptDispatched = try container.decodeIfPresent(Bool.self, forKey: .reviewPromptDispatched) ?? true
Expand Down
33 changes: 32 additions & 1 deletion Packages/CrowCore/Tests/CrowCoreTests/SessionModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Testing
#expect(session.ticketTitle == nil)
#expect(session.ticketNumber == nil)
#expect(session.provider == nil)
#expect(session.codeProvider == nil)
#expect(session.createdAt <= Date())
#expect(session.updatedAt <= Date())
}
Expand All @@ -22,7 +23,8 @@ import Testing
ticketURL: "https://github.com/org/repo/issues/42",
ticketTitle: "Fix the thing",
ticketNumber: 42,
provider: .github
provider: .github,
codeProvider: .gitlab
)
let data = try JSONEncoder().encode(session)
let decoded = try JSONDecoder().decode(Session.self, from: data)
Expand All @@ -34,6 +36,7 @@ import Testing
#expect(decoded.ticketTitle == "Fix the thing")
#expect(decoded.ticketNumber == 42)
#expect(decoded.provider == .github)
#expect(decoded.codeProvider == .gitlab)
}

@Test func sessionCodableWithNilOptionals() throws {
Expand All @@ -46,6 +49,34 @@ import Testing
#expect(decoded.ticketTitle == nil)
#expect(decoded.ticketNumber == nil)
#expect(decoded.provider == nil)
#expect(decoded.codeProvider == nil)
}

@Test func sessionBackwardCompatDecodingWithoutCodeProvider() throws {
// Persisted state.json predating CROW-414 has no `codeProvider`. Decode
// must succeed and default the field to nil so the runtime fallback
// (`session.codeProvider ?? session.provider`) routes legacy sessions
// to the same backend they used pre-split.
let id = UUID()
let date = Date()
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime]
let dateStr = formatter.string(from: date)
let json: [String: Any] = [
"id": id.uuidString,
"name": "legacy",
"status": "active",
"kind": "work",
"provider": "github",
"createdAt": dateStr,
"updatedAt": dateStr,
]
let data = try JSONSerialization.data(withJSONObject: json)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let session = try decoder.decode(Session.self, from: data)
#expect(session.provider == .github)
#expect(session.codeProvider == nil)
}

@Test func sessionBackwardCompatDecodingLastReviewedHeadSha() throws {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ import Testing
ticketURL: "https://github.com/org/repo/issues/42",
ticketTitle: "Fix the bug",
ticketNumber: 42,
provider: .github
provider: .github,
codeProvider: .gitlab
)
repo.save(session)

Expand All @@ -165,6 +166,7 @@ import Testing
#expect(found?.ticketTitle == "Fix the bug")
#expect(found?.ticketNumber == 42)
#expect(found?.provider == .github)
#expect(found?.codeProvider == .gitlab)
#expect(found?.status == .inReview)
}

Expand Down
6 changes: 3 additions & 3 deletions Sources/Crow/App/SessionService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1144,14 +1144,14 @@ final class SessionService {
}

/// Check for a pull request on a branch and return a link if found.
private func findPRLink(branch: String, repoPath: String, sessionID: UUID) async -> SessionLink? {
private func findPRLink(branch: String, repoPath: String, sessionID: UUID, provider: Provider) async -> SessionLink? {
guard let repoSlug = resolveRepoSlug(repoPath: repoPath) else { return nil }
// Prefer CodeBackend.linkedPR (ADR 0005) when a manager is wired; fall
// back to the inline `gh pr list` shell-out only when no manager is
// available — otherwise we'd issue the identical command twice on the
// common "no PR exists" path.
if let manager = providerManager {
guard let backend = manager.codeBackend(for: .github),
guard let backend = manager.codeBackend(for: provider),
let pr = try? await backend.linkedPR(repo: repoSlug, branch: branch) else {
return nil
}
Expand Down Expand Up @@ -1207,7 +1207,7 @@ final class SessionService {
let label = ticket.number.map { "Issue #\($0)" } ?? "Issue"
links.append(SessionLink(sessionID: session.id, label: label, url: ticketURL, linkType: .ticket))
}
if let prLink = await findPRLink(branch: branch, repoPath: repoPath, sessionID: session.id) {
if let prLink = await findPRLink(branch: branch, repoPath: repoPath, sessionID: session.id, provider: session.codeProvider ?? session.provider ?? .github) {
links.append(prLink)
}

Expand Down
Loading