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
4 changes: 2 additions & 2 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ let package = Package(
.package(url: "https://github.com/modelcontextprotocol/swift-sdk.git", exact: "0.12.1"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.97.1"),
.package(url: "https://github.com/lynnswap/ObservationBridge.git", .upToNextMinor(from: "0.12.0")),
// CodexKit has no tagged releases yet; pin the reviewed main revision
// (lynnswap/CodexKit#16 merge).
.package(url: "https://github.com/lynnswap/CodexKit.git", revision: "6de9e019d35095390252102bd30b069e1090c874"),
// CodexKit has no tagged releases yet; pin the final review response
// contract fix (lynnswap/CodexKit#17).
.package(url: "https://github.com/lynnswap/CodexKit.git", revision: "23276c81cf160b773b3f9fb5ae710d257838cad0"),
],
targets: [
.target(
Expand Down
58 changes: 58 additions & 0 deletions Tests/CodexReviewAppServerTests/AppServerClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,62 @@ struct AppServerClientTests {
#expect(try await iterator.next() == .completed(finalReview: "Looks good."))
}

@Test func backendCompletesReviewFromFinalAnswerDeltaWhenTerminalPayloadIsSparse() async throws {
let runtime = try await CodexAppServerTestRuntime.start()
try await runtime.transport.enqueueThreadStart(threadID: "thread-1", model: "gpt-5")
try await runtime.transport.enqueueReviewStart(turnID: "turn-1", reviewThreadID: "review-thread")
await runtime.transport.waitForNotificationStreamCount(1)
let backend = AppServerCodexReviewBackend(appServer: runtime.server)

let attempt = try await backend.startReview(makeReviewStart(target: .baseBranch("main")))
var iterator = eventSequence(attempt).makeAsyncIterator()

try await runtime.transport.emitServerNotification(
method: "item/agentMessage/delta",
params: TestDeltaNotification(
threadID: "review-thread",
turnID: "turn-1",
itemID: "msg-1",
delta: "Looks good.",
phase: "final_answer"
)
)
try await runtime.transport.emitServerNotification(
method: "turn/completed",
params: TestTurnNotification(
threadID: "review-thread",
turn: .init(id: "turn-1", status: "completed")
)
)

#expect(
try await iterator.next() == .started(turnID: "turn-1", reviewThreadID: "review-thread", model: "gpt-5"))
#expect(try await iterator.next() == .completed(finalReview: "Looks good."))
}

@Test func backendFailsCompletedReviewWithoutFinalAnswer() async throws {
let runtime = try await CodexAppServerTestRuntime.start()
try await runtime.transport.enqueueThreadStart(threadID: "thread-1", model: "gpt-5")
try await runtime.transport.enqueueReviewStart(turnID: "turn-1", reviewThreadID: "review-thread")
await runtime.transport.waitForNotificationStreamCount(1)
let backend = AppServerCodexReviewBackend(appServer: runtime.server)

let attempt = try await backend.startReview(makeReviewStart(target: .baseBranch("main")))
var iterator = eventSequence(attempt).makeAsyncIterator()

try await runtime.transport.emitServerNotification(
method: "turn/completed",
params: TestTurnNotification(
threadID: "review-thread",
turn: .init(id: "turn-1", status: "completed")
)
)

#expect(
try await iterator.next() == .started(turnID: "turn-1", reviewThreadID: "review-thread", model: "gpt-5"))
#expect(try await iterator.next() == .failed("Review completed without a final response."))
}

@Test func backendIgnoresAgentMessageDeltasInLifecycleStream() async throws {
let runtime = try await CodexAppServerTestRuntime.start()
try await runtime.transport.enqueueThreadStart(threadID: "thread-1", model: "gpt-5")
Expand Down Expand Up @@ -593,12 +649,14 @@ private struct TestDeltaNotification: Encodable, Sendable {
var turnID: String
var itemID: String
var delta: String
var phase: String?

enum CodingKeys: String, CodingKey {
case threadID = "threadId"
case turnID = "turnId"
case itemID = "itemId"
case delta
case phase
}
}

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.