




















@@ -11,6 +11,16 @@ private func chatTextMessage(role: String, text: String, timestamp: Double) -> A
1111])
1212}
131314+private func chatErrorMessage(role: String, errorMessage: String, timestamp: Double) -> AnyCodable {
15+AnyCodable([
16+"role": role,
17+"content": [],
18+"timestamp": timestamp,
19+"stopReason": "error",
20+"errorMessage": errorMessage,
21+])
22+}
23+1424private func historyPayload(
1525 sessionKey: String = "main",
1626 sessionId: String? = "sess-main",
@@ -454,6 +464,76 @@ extension TestChatTransportState {
454464}
455465456466@Suite struct ChatViewModelTests {
467+@Test func displaysErrorMessageFallbackOnlyForAssistantErrorTurns() throws {
468+func decodeMessage(role: String, stopReason: String, contentText: String? = nil) throws -> OpenClawChatMessage {
469+let contentJSON = contentText.map { #"[{"type":"text","text":"\#($0)"}]"# } ?? "[]"
470+let data = """
471+ {
472+ "role": "\(role)",
473+ "content": \(contentJSON),
474+ "timestamp": 1,
475+ "stopReason": "\(stopReason)",
476+ "errorMessage": "stale provider failure"
477+ }
478+ """.data(using: .utf8)!
479+return try JSONDecoder().decode(OpenClawChatMessage.self, from: data)
480+}
481+482+let assistantError = try decodeMessage(role: "assistant", stopReason: "error")
483+ #expect(assistantError.content.isEmpty)
484+ #expect(
485+OpenClawChatMessage.errorDisplayText(
486+ role: assistantError.role,
487+ stopReason: assistantError.stopReason,
488+ errorMessage: assistantError.errorMessage) == "stale provider failure")
489+ #expect(
490+OpenClawChatMessage.displayText(
491+ contentText: "",
492+ role: assistantError.role,
493+ stopReason: assistantError.stopReason,
494+ errorMessage: assistantError.errorMessage) == "stale provider failure")
495+496+let sentinelAssistant = try decodeMessage(
497+ role: "assistant",
498+ stopReason: "error",
499+ contentText: "[assistant turn failed before producing content]")
500+ #expect(
501+OpenClawChatMessage.displayText(
502+ contentText: sentinelAssistant.content.compactMap(\.text).joined(separator: "\n"),
503+ role: sentinelAssistant.role,
504+ stopReason: sentinelAssistant.stopReason,
505+ errorMessage: sentinelAssistant.errorMessage) == "stale provider failure")
506+507+let partialAssistant = try decodeMessage(
508+ role: "assistant",
509+ stopReason: "error",
510+ contentText: "partial answer")
511+ #expect(
512+OpenClawChatMessage.displayText(
513+ contentText: partialAssistant.content.compactMap(\.text).joined(separator: "\n"),
514+ role: partialAssistant.role,
515+ stopReason: partialAssistant.stopReason,
516+ errorMessage: partialAssistant.errorMessage) == "partial answer")
517+518+let stoppedAssistant = try decodeMessage(role: "assistant", stopReason: "stop")
519+ #expect(stoppedAssistant.errorMessage == "stale provider failure")
520+ #expect(stoppedAssistant.content.isEmpty)
521+ #expect(
522+OpenClawChatMessage.errorDisplayText(
523+ role: stoppedAssistant.role,
524+ stopReason: stoppedAssistant.stopReason,
525+ errorMessage: stoppedAssistant.errorMessage) == nil)
526+527+let toolUseAssistant = try decodeMessage(role: "assistant", stopReason: "toolUse")
528+ #expect(toolUseAssistant.errorMessage == "stale provider failure")
529+ #expect(toolUseAssistant.content.isEmpty)
530+ #expect(
531+OpenClawChatMessage.errorDisplayText(
532+ role: toolUseAssistant.role,
533+ stopReason: toolUseAssistant.stopReason,
534+ errorMessage: toolUseAssistant.errorMessage) == nil)
535+}
536+457537@Test func streamsAssistantAndClearsOnFinal() async throws {
458538let sessionId = "sess-main"
459539let history1 = historyPayload(sessionId: sessionId)
@@ -665,6 +745,51 @@ extension TestChatTransportState {
665745}
666746}
667747748+@Test func surfacesAssistantErrorMessageAfterOwnRunRefresh() async throws {
749+let now = Date().timeIntervalSince1970 * 1000
750+let history1 = historyPayload()
751+let history2 = historyPayload(
752+ messages: [
753+chatErrorMessage(
754+ role: "assistant",
755+ errorMessage: "You have hit your ChatGPT usage limit (plus plan). Try again in ~28 min.",
756+ timestamp: now),
757+])
758+759+let (transport, vm) = await makeViewModel(historyResponses: [history1, history2])
760+try await loadAndWaitBootstrap(vm: vm)
761+762+await sendUserMessage(vm)
763+try await waitUntil("pending run starts") { await MainActor.run { vm.pendingRunCount == 1 } }
764+765+let runId = try #require(await transport.lastSentRunId())
766+ transport.emit(
767+.chat(
768+OpenClawChatEventPayload(
769+ runId: runId,
770+ sessionKey: "main",
771+ state: "error",
772+ message: nil,
773+ errorMessage: "You have hit your ChatGPT usage limit (plus plan). Try again in ~28 min.")))
774+775+try await waitUntil("pending run clears after error") {
776+await MainActor.run { vm.pendingRunCount == 0 }
777+}
778+try await waitUntil("history refresh shows assistant error message") {
779+await MainActor.run {
780+ vm.messages.contains(where: { message in
781+ message.role == "assistant" &&
782+OpenClawChatMessage.displayText(
783+ contentText: message.content.compactMap(\.text).joined(separator: "\n"),
784+ role: message.role,
785+ stopReason: message.stopReason,
786+ errorMessage: message.errorMessage)
787+.contains("You have hit your ChatGPT usage limit")
788+})
789+}
790+}
791+}
792+668793@Test func acceptsCanonicalSessionKeyEventsForExternalRuns() async throws {
669794let now = Date().timeIntervalSince1970 * 1000
670795let history1 = historyPayload(messages: [chatTextMessage(role: "user", text: "first", timestamp: now)])
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。