File size: 8,612 Bytes
b8370f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | import Foundation
import OpenClawKit
import OpenClawProtocol
import Testing
@testable import OpenClawChatUI
@MainActor
struct ChatModelSignInTests {
@Test func `lost connection retires sign-in even when captured cleanup cannot run`() async throws {
var current = true
var cleanupAttempts = 0
var catalogRefreshes = 0
let context = OpenClawChatModelSignInContext(
agentID: "fixture-agent",
request: { method, params in
switch method {
case "models.authStatus":
return Data(
#"{"providers":[],"providerCapabilities":[{"loginOptions":[{"id":"fixture/device","label":"Sign in"}]}]}"#
.utf8)
case "models.authLogin":
return try JSONEncoder().encode(WizardStartResult(
sessionid: #require(params["sessionId"]?.stringValue), done: false,
step: WizardStep(id: "code", type: AnyCodable("note"), executor: AnyCodable("client"))))
case "wizard.next":
current = false
throw CancellationError()
case "wizard.cancel":
cleanupAttempts += 1
throw GatewayNodeSessionRequestError.routeChangedBeforeDispatch
default:
Issue.record("Unexpected sign-in request: \(method)")
throw CancellationError()
}
},
isCurrent: { current })
let model = ChatModelSignInModel(context: context, onAuthChanged: { catalogRefreshes += 1 })
await model.refresh()
try await model.start(#require(model.authStatus?.loginOptions.first))
#expect(model.sessionID != nil)
await model.answer()
#expect(model.message == "The connection changed. Close sign-in and open it again.")
await model.close()
#expect(model.sessionID == nil)
#expect(model.step == nil)
#expect(model.authStatus == nil)
#expect(!model.busy)
#expect(cleanupAttempts == 1)
#expect(catalogRefreshes == 0)
}
@Test func `closing during admission cancels the old wizard without publishing into the new session`() async throws {
let started = AsyncStream<Void>.makeStream()
var pendingLogin: CheckedContinuation<Data, Never>?
var current = true
var admitted = false
var statusReads = 0
var newSessionCatalogRefreshes = 0
var cancelledSessionIDs: [String] = []
let context = OpenClawChatModelSignInContext(
agentID: "old-agent",
request: { method, params in
switch method {
case "models.authStatus":
statusReads += 1
return Data(
#"{"providers":[],"providerCapabilities":[{"loginOptions":[{"id":"fixture/device","label":"Sign in"}]}]}"#
.utf8)
case "models.authLogin":
return await withCheckedContinuation { continuation in
pendingLogin = continuation
started.continuation.yield()
}
case "wizard.cancel":
try cancelledSessionIDs.append(#require(params["sessionId"]?.stringValue))
#expect(params["closeInput"]?.value as? Bool == true)
if !admitted {
throw GatewayResponseError(
method: method, code: "INVALID_REQUEST", message: "Not admitted yet",
details: ["code": AnyCodable("WIZARD_NOT_FOUND")])
}
return try JSONEncoder().encode(WizardStatusResult(status: AnyCodable("cancelled")))
default:
Issue.record("Stale sign-in must not request \(method)")
throw CancellationError()
}
},
isCurrent: { current })
let model = ChatModelSignInModel(context: context, onAuthChanged: { newSessionCatalogRefreshes += 1 })
await model.refresh()
let option = try #require(model.authStatus?.loginOptions.first)
let login = Task { await model.start(option) }
var events = started.stream.makeAsyncIterator()
_ = await events.next()
let oldSessionID = try #require(model.sessionID)
let response = try JSONEncoder().encode(WizardStartResult(sessionid: oldSessionID, done: false))
current = false
await model.close()
admitted = true
pendingLogin?.resume(returning: response)
await login.value
started.continuation.finish()
#expect(cancelledSessionIDs == [oldSessionID, oldSessionID])
#expect(model.sessionID == nil)
#expect(model.step == nil)
#expect(statusReads == 1)
#expect(newSessionCatalogRefreshes == 0)
}
@Test(arguments: ["done", "error", "cancelled"])
func `device code is not login completion and terminal errors still refresh published state`(
terminalStatus: String) async throws
{
var loginFinished = false
var catalogRefreshes = 0
var statusReads = 0
let context = OpenClawChatModelSignInContext(
agentID: "fixture-agent",
request: { method, params in
switch method {
case "models.authStatus":
#expect(params["agentId"]?.stringValue == "fixture-agent")
statusReads += 1
let status = loginFinished ? "ok" : "missing"
return Data("""
{"providers":[{"provider":"fixture","displayName":"Fixture","status":"\(status)"}],
"providerCapabilities":[{"loginOptions":[{"id":"fixture/device","label":"Sign in"}]}]}
""".utf8)
case "models.authLogin":
#expect(params["agentId"]?.stringValue == "fixture-agent")
#expect(params["authChoice"]?.stringValue == "fixture/device")
return try JSONEncoder().encode(WizardStartResult(
sessionid: #require(params["sessionId"]?.stringValue), done: false))
case "wizard.cancel":
#expect(terminalStatus == "cancelled")
return try JSONEncoder().encode(WizardStatusResult(status: AnyCodable("cancelled")))
case "wizard.next":
if params["answer"] != nil {
loginFinished = true
return try JSONEncoder().encode(WizardNextResult(
done: true, status: AnyCodable(terminalStatus),
error: terminalStatus == "error" ? "Later setup failed" : nil))
}
return try JSONEncoder().encode(WizardNextResult(
done: false,
step: WizardStep(
id: "code", type: AnyCodable("note"), executor: AnyCodable("client"),
devicecode: ["code": AnyCodable("ABCD-1234")]),
status: AnyCodable("running")))
default:
Issue.record("Unexpected sign-in request: \(method)")
throw CancellationError()
}
},
isCurrent: { true })
let model = ChatModelSignInModel(context: context, onAuthChanged: { catalogRefreshes += 1 })
await model.refresh()
let option = try #require(model.authStatus?.loginOptions.first)
await model.start(option)
#expect(parseWizardDeviceCode(model.step?.devicecode)?.code == "ABCD-1234")
#expect(model.sessionID != nil)
#expect(statusReads == 1)
#expect(catalogRefreshes == 0)
#expect(model.message == nil)
if terminalStatus == "cancelled" {
await model.cancel()
} else {
await model.answer()
}
#expect(model.sessionID == nil)
#expect(model.step == nil)
#expect(model.authStatus?.providers.first?.status == (terminalStatus == "cancelled" ? "missing" : "ok"))
#expect(model.message == (terminalStatus == "done"
? "Sign-in finished."
: "Sign-in ended. Review the account status before trying again."))
#expect(statusReads == 2)
#expect(catalogRefreshes == 1)
}
}
|