fix(mac): adopt canonical session key and add reset triggers (#10898)
Add shared native chat handling for /new, /reset, and /clear. This also aligns main session key handling in the shared chat UI and includes follow-up test and CI fixes needed to keep the branch mergeable. Co-authored-by: Nachx639 <71144023+Nachx639@users.noreply.github.com> Co-authored-by: Luke <92253590+ImLukeF@users.noreply.github.com>
This commit is contained in:
@@ -74,6 +74,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Models/OpenRouter native ids: canonicalize native OpenRouter model keys across config writes, runtime lookups, fallback management, and `models list --plain`, and migrate legacy duplicated `openrouter/openrouter/...` config entries forward on write.
|
- Models/OpenRouter native ids: canonicalize native OpenRouter model keys across config writes, runtime lookups, fallback management, and `models list --plain`, and migrate legacy duplicated `openrouter/openrouter/...` config entries forward on write.
|
||||||
- Gateway/hooks: bucket hook auth failures by forwarded client IP behind trusted proxies and warn when `hooks.allowedAgentIds` leaves hook routing unrestricted.
|
- Gateway/hooks: bucket hook auth failures by forwarded client IP behind trusted proxies and warn when `hooks.allowedAgentIds` leaves hook routing unrestricted.
|
||||||
- Agents/compaction: skip the post-compaction `cache-ttl` marker write when a compaction completed in the same attempt, preventing the next turn from immediately triggering a second tiny compaction. (#28548) thanks @MoerAI.
|
- Agents/compaction: skip the post-compaction `cache-ttl` marker write when a compaction completed in the same attempt, preventing the next turn from immediately triggering a second tiny compaction. (#28548) thanks @MoerAI.
|
||||||
|
- Native chat/macOS: add `/new`, `/reset`, and `/clear` reset triggers, keep shared main-session aliases aligned, and ignore stale model-selection completions so native chat state stays in sync across reset and fast model changes. (#10898) Thanks @Nachx639.
|
||||||
|
|
||||||
## 2026.3.11
|
## 2026.3.11
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ struct IOSGatewayChatTransport: OpenClawChatTransport, Sendable {
|
|||||||
// (chat.subscribe is a node event, not an operator RPC method.)
|
// (chat.subscribe is a node event, not an operator RPC method.)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resetSession(sessionKey: String) async throws {
|
||||||
|
struct Params: Codable { var key: String }
|
||||||
|
let data = try JSONEncoder().encode(Params(key: sessionKey))
|
||||||
|
let json = String(data: data, encoding: .utf8)
|
||||||
|
_ = try await self.gateway.request(method: "sessions.reset", paramsJSON: json, timeoutSeconds: 10)
|
||||||
|
}
|
||||||
|
|
||||||
func requestHistory(sessionKey: String) async throws -> OpenClawChatHistoryPayload {
|
func requestHistory(sessionKey: String) async throws -> OpenClawChatHistoryPayload {
|
||||||
struct Params: Codable { var sessionKey: String }
|
struct Params: Codable { var sessionKey: String }
|
||||||
let data = try JSONEncoder().encode(Params(sessionKey: sessionKey))
|
let data = try JSONEncoder().encode(Params(sessionKey: sessionKey))
|
||||||
|
|||||||
@@ -26,5 +26,10 @@ import Testing
|
|||||||
_ = try await transport.requestHealth(timeoutMs: 250)
|
_ = try await transport.requestHealth(timeoutMs: 250)
|
||||||
Issue.record("Expected requestHealth to throw when gateway not connected")
|
Issue.record("Expected requestHealth to throw when gateway not connected")
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await transport.resetSession(sessionKey: "node-test")
|
||||||
|
Issue.record("Expected resetSession to throw when gateway not connected")
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,23 @@ struct MacGatewayChatTransport: OpenClawChatTransport {
|
|||||||
method: "sessions.list",
|
method: "sessions.list",
|
||||||
params: params,
|
params: params,
|
||||||
timeoutMs: 15000)
|
timeoutMs: 15000)
|
||||||
return try JSONDecoder().decode(OpenClawChatSessionsListResponse.self, from: data)
|
let decoded = try JSONDecoder().decode(OpenClawChatSessionsListResponse.self, from: data)
|
||||||
|
let mainSessionKey = await GatewayConnection.shared.cachedMainSessionKey()
|
||||||
|
let defaults = decoded.defaults.map {
|
||||||
|
OpenClawChatSessionsDefaults(
|
||||||
|
model: $0.model,
|
||||||
|
contextTokens: $0.contextTokens,
|
||||||
|
mainSessionKey: mainSessionKey)
|
||||||
|
} ?? OpenClawChatSessionsDefaults(
|
||||||
|
model: nil,
|
||||||
|
contextTokens: nil,
|
||||||
|
mainSessionKey: mainSessionKey)
|
||||||
|
return OpenClawChatSessionsListResponse(
|
||||||
|
ts: decoded.ts,
|
||||||
|
path: decoded.path,
|
||||||
|
count: decoded.count,
|
||||||
|
defaults: defaults,
|
||||||
|
sessions: decoded.sessions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setSessionModel(sessionKey: String, model: String?) async throws {
|
func setSessionModel(sessionKey: String, model: String?) async throws {
|
||||||
@@ -103,6 +119,13 @@ struct MacGatewayChatTransport: OpenClawChatTransport {
|
|||||||
try await GatewayConnection.shared.healthOK(timeoutMs: timeoutMs)
|
try await GatewayConnection.shared.healthOK(timeoutMs: timeoutMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resetSession(sessionKey: String) async throws {
|
||||||
|
_ = try await GatewayConnection.shared.request(
|
||||||
|
method: "sessions.reset",
|
||||||
|
params: ["key": AnyCodable(sessionKey)],
|
||||||
|
timeoutMs: 10000)
|
||||||
|
}
|
||||||
|
|
||||||
func events() -> AsyncStream<OpenClawChatTransportEvent> {
|
func events() -> AsyncStream<OpenClawChatTransportEvent> {
|
||||||
AsyncStream { continuation in
|
AsyncStream { continuation in
|
||||||
let task = Task {
|
let task = Task {
|
||||||
|
|||||||
@@ -1322,6 +1322,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||||||
public let key: String
|
public let key: String
|
||||||
public let label: AnyCodable?
|
public let label: AnyCodable?
|
||||||
public let thinkinglevel: AnyCodable?
|
public let thinkinglevel: AnyCodable?
|
||||||
|
public let fastmode: AnyCodable?
|
||||||
public let verboselevel: AnyCodable?
|
public let verboselevel: AnyCodable?
|
||||||
public let reasoninglevel: AnyCodable?
|
public let reasoninglevel: AnyCodable?
|
||||||
public let responseusage: AnyCodable?
|
public let responseusage: AnyCodable?
|
||||||
@@ -1343,6 +1344,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||||||
key: String,
|
key: String,
|
||||||
label: AnyCodable?,
|
label: AnyCodable?,
|
||||||
thinkinglevel: AnyCodable?,
|
thinkinglevel: AnyCodable?,
|
||||||
|
fastmode: AnyCodable?,
|
||||||
verboselevel: AnyCodable?,
|
verboselevel: AnyCodable?,
|
||||||
reasoninglevel: AnyCodable?,
|
reasoninglevel: AnyCodable?,
|
||||||
responseusage: AnyCodable?,
|
responseusage: AnyCodable?,
|
||||||
@@ -1363,6 +1365,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||||||
self.key = key
|
self.key = key
|
||||||
self.label = label
|
self.label = label
|
||||||
self.thinkinglevel = thinkinglevel
|
self.thinkinglevel = thinkinglevel
|
||||||
|
self.fastmode = fastmode
|
||||||
self.verboselevel = verboselevel
|
self.verboselevel = verboselevel
|
||||||
self.reasoninglevel = reasoninglevel
|
self.reasoninglevel = reasoninglevel
|
||||||
self.responseusage = responseusage
|
self.responseusage = responseusage
|
||||||
@@ -1385,6 +1388,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||||||
case key
|
case key
|
||||||
case label
|
case label
|
||||||
case thinkinglevel = "thinkingLevel"
|
case thinkinglevel = "thinkingLevel"
|
||||||
|
case fastmode = "fastMode"
|
||||||
case verboselevel = "verboseLevel"
|
case verboselevel = "verboseLevel"
|
||||||
case reasoninglevel = "reasoningLevel"
|
case reasoninglevel = "reasoningLevel"
|
||||||
case responseusage = "responseUsage"
|
case responseusage = "responseUsage"
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ public struct OpenClawChatModelChoice: Identifiable, Codable, Sendable, Hashable
|
|||||||
public struct OpenClawChatSessionsDefaults: Codable, Sendable {
|
public struct OpenClawChatSessionsDefaults: Codable, Sendable {
|
||||||
public let model: String?
|
public let model: String?
|
||||||
public let contextTokens: Int?
|
public let contextTokens: Int?
|
||||||
|
public let mainSessionKey: String?
|
||||||
|
|
||||||
|
public init(model: String?, contextTokens: Int?, mainSessionKey: String? = nil) {
|
||||||
|
self.model = model
|
||||||
|
self.contextTokens = contextTokens
|
||||||
|
self.mainSessionKey = mainSessionKey
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct OpenClawChatSessionEntry: Codable, Identifiable, Sendable, Hashable {
|
public struct OpenClawChatSessionEntry: Codable, Identifiable, Sendable, Hashable {
|
||||||
@@ -69,4 +76,18 @@ public struct OpenClawChatSessionsListResponse: Codable, Sendable {
|
|||||||
public let count: Int?
|
public let count: Int?
|
||||||
public let defaults: OpenClawChatSessionsDefaults?
|
public let defaults: OpenClawChatSessionsDefaults?
|
||||||
public let sessions: [OpenClawChatSessionEntry]
|
public let sessions: [OpenClawChatSessionEntry]
|
||||||
|
|
||||||
|
public init(
|
||||||
|
ts: Double?,
|
||||||
|
path: String?,
|
||||||
|
count: Int?,
|
||||||
|
defaults: OpenClawChatSessionsDefaults?,
|
||||||
|
sessions: [OpenClawChatSessionEntry])
|
||||||
|
{
|
||||||
|
self.ts = ts
|
||||||
|
self.path = path
|
||||||
|
self.count = count
|
||||||
|
self.defaults = defaults
|
||||||
|
self.sessions = sessions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,11 +27,19 @@ public protocol OpenClawChatTransport: Sendable {
|
|||||||
func events() -> AsyncStream<OpenClawChatTransportEvent>
|
func events() -> AsyncStream<OpenClawChatTransportEvent>
|
||||||
|
|
||||||
func setActiveSessionKey(_ sessionKey: String) async throws
|
func setActiveSessionKey(_ sessionKey: String) async throws
|
||||||
|
func resetSession(sessionKey: String) async throws
|
||||||
}
|
}
|
||||||
|
|
||||||
extension OpenClawChatTransport {
|
extension OpenClawChatTransport {
|
||||||
public func setActiveSessionKey(_: String) async throws {}
|
public func setActiveSessionKey(_: String) async throws {}
|
||||||
|
|
||||||
|
public func resetSession(sessionKey _: String) async throws {
|
||||||
|
throw NSError(
|
||||||
|
domain: "OpenClawChatTransport",
|
||||||
|
code: 0,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: "sessions.reset not supported by this transport"])
|
||||||
|
}
|
||||||
|
|
||||||
public func abortRun(sessionKey _: String, runId _: String) async throws {
|
public func abortRun(sessionKey _: String, runId _: String) async throws {
|
||||||
throw NSError(
|
throw NSError(
|
||||||
domain: "OpenClawChatTransport",
|
domain: "OpenClawChatTransport",
|
||||||
|
|||||||
@@ -138,21 +138,23 @@ public final class OpenClawChatViewModel {
|
|||||||
let now = Date().timeIntervalSince1970 * 1000
|
let now = Date().timeIntervalSince1970 * 1000
|
||||||
let cutoff = now - (24 * 60 * 60 * 1000)
|
let cutoff = now - (24 * 60 * 60 * 1000)
|
||||||
let sorted = self.sessions.sorted { ($0.updatedAt ?? 0) > ($1.updatedAt ?? 0) }
|
let sorted = self.sessions.sorted { ($0.updatedAt ?? 0) > ($1.updatedAt ?? 0) }
|
||||||
|
let mainSessionKey = self.resolvedMainSessionKey
|
||||||
|
|
||||||
var result: [OpenClawChatSessionEntry] = []
|
var result: [OpenClawChatSessionEntry] = []
|
||||||
var included = Set<String>()
|
var included = Set<String>()
|
||||||
|
|
||||||
// Always show the main session first, even if it hasn't been updated recently.
|
// Always show the resolved main session first, even if it hasn't been updated recently.
|
||||||
if let main = sorted.first(where: { $0.key == "main" }) {
|
if let main = sorted.first(where: { $0.key == mainSessionKey }) {
|
||||||
result.append(main)
|
result.append(main)
|
||||||
included.insert(main.key)
|
included.insert(main.key)
|
||||||
} else {
|
} else {
|
||||||
result.append(self.placeholderSession(key: "main"))
|
result.append(self.placeholderSession(key: mainSessionKey))
|
||||||
included.insert("main")
|
included.insert(mainSessionKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
for entry in sorted {
|
for entry in sorted {
|
||||||
guard !included.contains(entry.key) else { continue }
|
guard !included.contains(entry.key) else { continue }
|
||||||
|
guard entry.key == self.sessionKey || !Self.isHiddenInternalSession(entry.key) else { continue }
|
||||||
guard (entry.updatedAt ?? 0) >= cutoff else { continue }
|
guard (entry.updatedAt ?? 0) >= cutoff else { continue }
|
||||||
result.append(entry)
|
result.append(entry)
|
||||||
included.insert(entry.key)
|
included.insert(entry.key)
|
||||||
@@ -169,6 +171,18 @@ public final class OpenClawChatViewModel {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var resolvedMainSessionKey: String {
|
||||||
|
let trimmed = self.sessionDefaults?.mainSessionKey?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return (trimmed?.isEmpty == false ? trimmed : nil) ?? "main"
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func isHiddenInternalSession(_ key: String) -> Bool {
|
||||||
|
let trimmed = key.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !trimmed.isEmpty else { return false }
|
||||||
|
return trimmed == "onboarding" || trimmed.hasSuffix(":onboarding")
|
||||||
|
}
|
||||||
|
|
||||||
public var showsModelPicker: Bool {
|
public var showsModelPicker: Bool {
|
||||||
!self.modelChoices.isEmpty
|
!self.modelChoices.isEmpty
|
||||||
}
|
}
|
||||||
@@ -365,10 +379,19 @@ public final class OpenClawChatViewModel {
|
|||||||
return "\(message.role)|\(timestamp)|\(text)"
|
return "\(message.role)|\(timestamp)|\(text)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static let resetTriggers: Set<String> = ["/new", "/reset", "/clear"]
|
||||||
|
|
||||||
private func performSend() async {
|
private func performSend() async {
|
||||||
guard !self.isSending else { return }
|
guard !self.isSending else { return }
|
||||||
let trimmed = self.input.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = self.input.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
guard !trimmed.isEmpty || !self.attachments.isEmpty else { return }
|
guard !trimmed.isEmpty || !self.attachments.isEmpty else { return }
|
||||||
|
|
||||||
|
if Self.resetTriggers.contains(trimmed.lowercased()) {
|
||||||
|
self.input = ""
|
||||||
|
await self.performReset()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let sessionKey = self.sessionKey
|
let sessionKey = self.sessionKey
|
||||||
|
|
||||||
guard self.healthOK else {
|
guard self.healthOK else {
|
||||||
@@ -499,6 +522,22 @@ public final class OpenClawChatViewModel {
|
|||||||
await self.bootstrap()
|
await self.bootstrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func performReset() async {
|
||||||
|
self.isLoading = true
|
||||||
|
self.errorText = nil
|
||||||
|
defer { self.isLoading = false }
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await self.transport.resetSession(sessionKey: self.sessionKey)
|
||||||
|
} catch {
|
||||||
|
self.errorText = error.localizedDescription
|
||||||
|
chatUILogger.error("session reset failed \(error.localizedDescription, privacy: .public)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await self.bootstrap()
|
||||||
|
}
|
||||||
|
|
||||||
private func performSelectThinkingLevel(_ level: String) async {
|
private func performSelectThinkingLevel(_ level: String) async {
|
||||||
let next = Self.normalizedThinkingLevel(level) ?? "off"
|
let next = Self.normalizedThinkingLevel(level) ?? "off"
|
||||||
guard next != self.thinkingLevel else { return }
|
guard next != self.thinkingLevel else { return }
|
||||||
@@ -549,7 +588,9 @@ public final class OpenClawChatViewModel {
|
|||||||
sessionKey: sessionKey,
|
sessionKey: sessionKey,
|
||||||
model: nextModelRef)
|
model: nextModelRef)
|
||||||
guard requestID == self.latestModelSelectionRequestIDsBySession[sessionKey] else {
|
guard requestID == self.latestModelSelectionRequestIDsBySession[sessionKey] else {
|
||||||
self.applySuccessfulModelSelection(next, sessionKey: sessionKey, syncSelection: false)
|
// Keep older successful patches as rollback state, but do not replay
|
||||||
|
// stale UI/session state over a newer in-flight or completed selection.
|
||||||
|
self.lastSuccessfulModelSelectionIDsBySession[sessionKey] = next
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.applySuccessfulModelSelection(next, sessionKey: sessionKey, syncSelection: true)
|
self.applySuccessfulModelSelection(next, sessionKey: sessionKey, syncSelection: true)
|
||||||
|
|||||||
@@ -1322,6 +1322,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||||||
public let key: String
|
public let key: String
|
||||||
public let label: AnyCodable?
|
public let label: AnyCodable?
|
||||||
public let thinkinglevel: AnyCodable?
|
public let thinkinglevel: AnyCodable?
|
||||||
|
public let fastmode: AnyCodable?
|
||||||
public let verboselevel: AnyCodable?
|
public let verboselevel: AnyCodable?
|
||||||
public let reasoninglevel: AnyCodable?
|
public let reasoninglevel: AnyCodable?
|
||||||
public let responseusage: AnyCodable?
|
public let responseusage: AnyCodable?
|
||||||
@@ -1343,6 +1344,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||||||
key: String,
|
key: String,
|
||||||
label: AnyCodable?,
|
label: AnyCodable?,
|
||||||
thinkinglevel: AnyCodable?,
|
thinkinglevel: AnyCodable?,
|
||||||
|
fastmode: AnyCodable?,
|
||||||
verboselevel: AnyCodable?,
|
verboselevel: AnyCodable?,
|
||||||
reasoninglevel: AnyCodable?,
|
reasoninglevel: AnyCodable?,
|
||||||
responseusage: AnyCodable?,
|
responseusage: AnyCodable?,
|
||||||
@@ -1363,6 +1365,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||||||
self.key = key
|
self.key = key
|
||||||
self.label = label
|
self.label = label
|
||||||
self.thinkinglevel = thinkinglevel
|
self.thinkinglevel = thinkinglevel
|
||||||
|
self.fastmode = fastmode
|
||||||
self.verboselevel = verboselevel
|
self.verboselevel = verboselevel
|
||||||
self.reasoninglevel = reasoninglevel
|
self.reasoninglevel = reasoninglevel
|
||||||
self.responseusage = responseusage
|
self.responseusage = responseusage
|
||||||
@@ -1385,6 +1388,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||||||
case key
|
case key
|
||||||
case label
|
case label
|
||||||
case thinkinglevel = "thinkingLevel"
|
case thinkinglevel = "thinkingLevel"
|
||||||
|
case fastmode = "fastMode"
|
||||||
case verboselevel = "verboseLevel"
|
case verboselevel = "verboseLevel"
|
||||||
case reasoninglevel = "reasoningLevel"
|
case reasoninglevel = "reasoningLevel"
|
||||||
case responseusage = "responseUsage"
|
case responseusage = "responseUsage"
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ private func makeViewModel(
|
|||||||
historyResponses: [OpenClawChatHistoryPayload],
|
historyResponses: [OpenClawChatHistoryPayload],
|
||||||
sessionsResponses: [OpenClawChatSessionsListResponse] = [],
|
sessionsResponses: [OpenClawChatSessionsListResponse] = [],
|
||||||
modelResponses: [[OpenClawChatModelChoice]] = [],
|
modelResponses: [[OpenClawChatModelChoice]] = [],
|
||||||
|
resetSessionHook: (@Sendable (String) async throws -> Void)? = nil,
|
||||||
setSessionModelHook: (@Sendable (String?) async throws -> Void)? = nil,
|
setSessionModelHook: (@Sendable (String?) async throws -> Void)? = nil,
|
||||||
setSessionThinkingHook: (@Sendable (String) async throws -> Void)? = nil,
|
setSessionThinkingHook: (@Sendable (String) async throws -> Void)? = nil,
|
||||||
initialThinkingLevel: String? = nil,
|
initialThinkingLevel: String? = nil,
|
||||||
@@ -93,6 +94,7 @@ private func makeViewModel(
|
|||||||
historyResponses: historyResponses,
|
historyResponses: historyResponses,
|
||||||
sessionsResponses: sessionsResponses,
|
sessionsResponses: sessionsResponses,
|
||||||
modelResponses: modelResponses,
|
modelResponses: modelResponses,
|
||||||
|
resetSessionHook: resetSessionHook,
|
||||||
setSessionModelHook: setSessionModelHook,
|
setSessionModelHook: setSessionModelHook,
|
||||||
setSessionThinkingHook: setSessionThinkingHook)
|
setSessionThinkingHook: setSessionThinkingHook)
|
||||||
let vm = await MainActor.run {
|
let vm = await MainActor.run {
|
||||||
@@ -199,6 +201,7 @@ private actor TestChatTransportState {
|
|||||||
var historyCallCount: Int = 0
|
var historyCallCount: Int = 0
|
||||||
var sessionsCallCount: Int = 0
|
var sessionsCallCount: Int = 0
|
||||||
var modelsCallCount: Int = 0
|
var modelsCallCount: Int = 0
|
||||||
|
var resetSessionKeys: [String] = []
|
||||||
var sentRunIds: [String] = []
|
var sentRunIds: [String] = []
|
||||||
var sentThinkingLevels: [String] = []
|
var sentThinkingLevels: [String] = []
|
||||||
var abortedRunIds: [String] = []
|
var abortedRunIds: [String] = []
|
||||||
@@ -211,6 +214,7 @@ private final class TestChatTransport: @unchecked Sendable, OpenClawChatTranspor
|
|||||||
private let historyResponses: [OpenClawChatHistoryPayload]
|
private let historyResponses: [OpenClawChatHistoryPayload]
|
||||||
private let sessionsResponses: [OpenClawChatSessionsListResponse]
|
private let sessionsResponses: [OpenClawChatSessionsListResponse]
|
||||||
private let modelResponses: [[OpenClawChatModelChoice]]
|
private let modelResponses: [[OpenClawChatModelChoice]]
|
||||||
|
private let resetSessionHook: (@Sendable (String) async throws -> Void)?
|
||||||
private let setSessionModelHook: (@Sendable (String?) async throws -> Void)?
|
private let setSessionModelHook: (@Sendable (String?) async throws -> Void)?
|
||||||
private let setSessionThinkingHook: (@Sendable (String) async throws -> Void)?
|
private let setSessionThinkingHook: (@Sendable (String) async throws -> Void)?
|
||||||
|
|
||||||
@@ -221,12 +225,14 @@ private final class TestChatTransport: @unchecked Sendable, OpenClawChatTranspor
|
|||||||
historyResponses: [OpenClawChatHistoryPayload],
|
historyResponses: [OpenClawChatHistoryPayload],
|
||||||
sessionsResponses: [OpenClawChatSessionsListResponse] = [],
|
sessionsResponses: [OpenClawChatSessionsListResponse] = [],
|
||||||
modelResponses: [[OpenClawChatModelChoice]] = [],
|
modelResponses: [[OpenClawChatModelChoice]] = [],
|
||||||
|
resetSessionHook: (@Sendable (String) async throws -> Void)? = nil,
|
||||||
setSessionModelHook: (@Sendable (String?) async throws -> Void)? = nil,
|
setSessionModelHook: (@Sendable (String?) async throws -> Void)? = nil,
|
||||||
setSessionThinkingHook: (@Sendable (String) async throws -> Void)? = nil)
|
setSessionThinkingHook: (@Sendable (String) async throws -> Void)? = nil)
|
||||||
{
|
{
|
||||||
self.historyResponses = historyResponses
|
self.historyResponses = historyResponses
|
||||||
self.sessionsResponses = sessionsResponses
|
self.sessionsResponses = sessionsResponses
|
||||||
self.modelResponses = modelResponses
|
self.modelResponses = modelResponses
|
||||||
|
self.resetSessionHook = resetSessionHook
|
||||||
self.setSessionModelHook = setSessionModelHook
|
self.setSessionModelHook = setSessionModelHook
|
||||||
self.setSessionThinkingHook = setSessionThinkingHook
|
self.setSessionThinkingHook = setSessionThinkingHook
|
||||||
var cont: AsyncStream<OpenClawChatTransportEvent>.Continuation!
|
var cont: AsyncStream<OpenClawChatTransportEvent>.Continuation!
|
||||||
@@ -301,6 +307,13 @@ private final class TestChatTransport: @unchecked Sendable, OpenClawChatTranspor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resetSession(sessionKey: String) async throws {
|
||||||
|
await self.state.resetSessionKeysAppend(sessionKey)
|
||||||
|
if let resetSessionHook = self.resetSessionHook {
|
||||||
|
try await resetSessionHook(sessionKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func setSessionThinking(sessionKey _: String, thinkingLevel: String) async throws {
|
func setSessionThinking(sessionKey _: String, thinkingLevel: String) async throws {
|
||||||
await self.state.patchedThinkingLevelsAppend(thinkingLevel)
|
await self.state.patchedThinkingLevelsAppend(thinkingLevel)
|
||||||
if let setSessionThinkingHook = self.setSessionThinkingHook {
|
if let setSessionThinkingHook = self.setSessionThinkingHook {
|
||||||
@@ -336,6 +349,10 @@ private final class TestChatTransport: @unchecked Sendable, OpenClawChatTranspor
|
|||||||
func patchedThinkingLevels() async -> [String] {
|
func patchedThinkingLevels() async -> [String] {
|
||||||
await self.state.patchedThinkingLevels
|
await self.state.patchedThinkingLevels
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resetSessionKeys() async -> [String] {
|
||||||
|
await self.state.resetSessionKeys
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TestChatTransportState {
|
extension TestChatTransportState {
|
||||||
@@ -370,6 +387,10 @@ extension TestChatTransportState {
|
|||||||
fileprivate func patchedThinkingLevelsAppend(_ v: String) {
|
fileprivate func patchedThinkingLevelsAppend(_ v: String) {
|
||||||
self.patchedThinkingLevels.append(v)
|
self.patchedThinkingLevels.append(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate func resetSessionKeysAppend(_ v: String) {
|
||||||
|
self.resetSessionKeys.append(v)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suite struct ChatViewModelTests {
|
@Suite struct ChatViewModelTests {
|
||||||
@@ -592,6 +613,151 @@ extension TestChatTransportState {
|
|||||||
#expect(keys == ["main", "custom"])
|
#expect(keys == ["main", "custom"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func sessionChoicesUseResolvedMainSessionKeyInsteadOfLiteralMain() async throws {
|
||||||
|
let now = Date().timeIntervalSince1970 * 1000
|
||||||
|
let recent = now - (30 * 60 * 1000)
|
||||||
|
let recentOlder = now - (90 * 60 * 1000)
|
||||||
|
let history = historyPayload(sessionKey: "Luke’s MacBook Pro", sessionId: "sess-main")
|
||||||
|
let sessions = OpenClawChatSessionsListResponse(
|
||||||
|
ts: now,
|
||||||
|
path: nil,
|
||||||
|
count: 2,
|
||||||
|
defaults: OpenClawChatSessionsDefaults(
|
||||||
|
model: nil,
|
||||||
|
contextTokens: nil,
|
||||||
|
mainSessionKey: "Luke’s MacBook Pro"),
|
||||||
|
sessions: [
|
||||||
|
OpenClawChatSessionEntry(
|
||||||
|
key: "Luke’s MacBook Pro",
|
||||||
|
kind: nil,
|
||||||
|
displayName: "Luke’s MacBook Pro",
|
||||||
|
surface: nil,
|
||||||
|
subject: nil,
|
||||||
|
room: nil,
|
||||||
|
space: nil,
|
||||||
|
updatedAt: recent,
|
||||||
|
sessionId: nil,
|
||||||
|
systemSent: nil,
|
||||||
|
abortedLastRun: nil,
|
||||||
|
thinkingLevel: nil,
|
||||||
|
verboseLevel: nil,
|
||||||
|
inputTokens: nil,
|
||||||
|
outputTokens: nil,
|
||||||
|
totalTokens: nil,
|
||||||
|
modelProvider: nil,
|
||||||
|
model: nil,
|
||||||
|
contextTokens: nil),
|
||||||
|
sessionEntry(key: "recent-1", updatedAt: recentOlder),
|
||||||
|
])
|
||||||
|
|
||||||
|
let (_, vm) = await makeViewModel(
|
||||||
|
sessionKey: "Luke’s MacBook Pro",
|
||||||
|
historyResponses: [history],
|
||||||
|
sessionsResponses: [sessions])
|
||||||
|
await MainActor.run { vm.load() }
|
||||||
|
try await waitUntil("sessions loaded") { await MainActor.run { !vm.sessions.isEmpty } }
|
||||||
|
|
||||||
|
let keys = await MainActor.run { vm.sessionChoices.map(\.key) }
|
||||||
|
#expect(keys == ["Luke’s MacBook Pro", "recent-1"])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func sessionChoicesHideInternalOnboardingSession() async throws {
|
||||||
|
let now = Date().timeIntervalSince1970 * 1000
|
||||||
|
let recent = now - (2 * 60 * 1000)
|
||||||
|
let recentOlder = now - (5 * 60 * 1000)
|
||||||
|
let history = historyPayload(sessionKey: "agent:main:main", sessionId: "sess-main")
|
||||||
|
let sessions = OpenClawChatSessionsListResponse(
|
||||||
|
ts: now,
|
||||||
|
path: nil,
|
||||||
|
count: 2,
|
||||||
|
defaults: OpenClawChatSessionsDefaults(
|
||||||
|
model: nil,
|
||||||
|
contextTokens: nil,
|
||||||
|
mainSessionKey: "agent:main:main"),
|
||||||
|
sessions: [
|
||||||
|
OpenClawChatSessionEntry(
|
||||||
|
key: "agent:main:onboarding",
|
||||||
|
kind: nil,
|
||||||
|
displayName: "Luke’s MacBook Pro",
|
||||||
|
surface: nil,
|
||||||
|
subject: nil,
|
||||||
|
room: nil,
|
||||||
|
space: nil,
|
||||||
|
updatedAt: recent,
|
||||||
|
sessionId: nil,
|
||||||
|
systemSent: nil,
|
||||||
|
abortedLastRun: nil,
|
||||||
|
thinkingLevel: nil,
|
||||||
|
verboseLevel: nil,
|
||||||
|
inputTokens: nil,
|
||||||
|
outputTokens: nil,
|
||||||
|
totalTokens: nil,
|
||||||
|
modelProvider: nil,
|
||||||
|
model: nil,
|
||||||
|
contextTokens: nil),
|
||||||
|
OpenClawChatSessionEntry(
|
||||||
|
key: "agent:main:main",
|
||||||
|
kind: nil,
|
||||||
|
displayName: "Luke’s MacBook Pro",
|
||||||
|
surface: nil,
|
||||||
|
subject: nil,
|
||||||
|
room: nil,
|
||||||
|
space: nil,
|
||||||
|
updatedAt: recentOlder,
|
||||||
|
sessionId: nil,
|
||||||
|
systemSent: nil,
|
||||||
|
abortedLastRun: nil,
|
||||||
|
thinkingLevel: nil,
|
||||||
|
verboseLevel: nil,
|
||||||
|
inputTokens: nil,
|
||||||
|
outputTokens: nil,
|
||||||
|
totalTokens: nil,
|
||||||
|
modelProvider: nil,
|
||||||
|
model: nil,
|
||||||
|
contextTokens: nil),
|
||||||
|
])
|
||||||
|
|
||||||
|
let (_, vm) = await makeViewModel(
|
||||||
|
sessionKey: "agent:main:main",
|
||||||
|
historyResponses: [history],
|
||||||
|
sessionsResponses: [sessions])
|
||||||
|
await MainActor.run { vm.load() }
|
||||||
|
try await waitUntil("sessions loaded") { await MainActor.run { !vm.sessions.isEmpty } }
|
||||||
|
|
||||||
|
let keys = await MainActor.run { vm.sessionChoices.map(\.key) }
|
||||||
|
#expect(keys == ["agent:main:main"])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func resetTriggerResetsSessionAndReloadsHistory() async throws {
|
||||||
|
let before = historyPayload(
|
||||||
|
messages: [
|
||||||
|
chatTextMessage(role: "assistant", text: "before reset", timestamp: 1),
|
||||||
|
])
|
||||||
|
let after = historyPayload(
|
||||||
|
messages: [
|
||||||
|
chatTextMessage(role: "assistant", text: "after reset", timestamp: 2),
|
||||||
|
])
|
||||||
|
|
||||||
|
let (transport, vm) = await makeViewModel(historyResponses: [before, after])
|
||||||
|
try await loadAndWaitBootstrap(vm: vm)
|
||||||
|
try await waitUntil("initial history loaded") {
|
||||||
|
await MainActor.run { vm.messages.first?.content.first?.text == "before reset" }
|
||||||
|
}
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
vm.input = "/new"
|
||||||
|
vm.send()
|
||||||
|
}
|
||||||
|
|
||||||
|
try await waitUntil("reset called") {
|
||||||
|
await transport.resetSessionKeys() == ["main"]
|
||||||
|
}
|
||||||
|
try await waitUntil("history reloaded") {
|
||||||
|
await MainActor.run { vm.messages.first?.content.first?.text == "after reset" }
|
||||||
|
}
|
||||||
|
#expect(await transport.lastSentRunId() == nil)
|
||||||
|
}
|
||||||
|
|
||||||
@Test func bootstrapsModelSelectionFromSessionAndDefaults() async throws {
|
@Test func bootstrapsModelSelectionFromSessionAndDefaults() async throws {
|
||||||
let now = Date().timeIntervalSince1970 * 1000
|
let now = Date().timeIntervalSince1970 * 1000
|
||||||
let history = historyPayload()
|
let history = historyPayload()
|
||||||
@@ -758,7 +924,8 @@ extension TestChatTransportState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#expect(await MainActor.run { vm.modelSelectionID } == "openai/gpt-5.4-pro")
|
#expect(await MainActor.run { vm.modelSelectionID } == "openai/gpt-5.4-pro")
|
||||||
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.model } == "openai/gpt-5.4-pro")
|
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.model } == "gpt-5.4-pro")
|
||||||
|
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.modelProvider } == "openai")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func sendWaitsForInFlightModelPatchToFinish() async throws {
|
@Test func sendWaitsForInFlightModelPatchToFinish() async throws {
|
||||||
@@ -852,11 +1019,15 @@ extension TestChatTransportState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try await waitUntil("older model completion wins after latest failure") {
|
try await waitUntil("older model completion wins after latest failure") {
|
||||||
await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.model == "openai/gpt-5.4" }
|
await MainActor.run {
|
||||||
|
vm.sessions.first(where: { $0.key == "main" })?.model == "gpt-5.4" &&
|
||||||
|
vm.sessions.first(where: { $0.key == "main" })?.modelProvider == "openai"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#expect(await MainActor.run { vm.modelSelectionID } == "openai/gpt-5.4")
|
#expect(await MainActor.run { vm.modelSelectionID } == "openai/gpt-5.4")
|
||||||
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.model } == "openai/gpt-5.4")
|
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.model } == "gpt-5.4")
|
||||||
|
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.modelProvider } == "openai")
|
||||||
#expect(await transport.patchedModels() == ["openai/gpt-5.4", "openai/gpt-5.4-pro"])
|
#expect(await transport.patchedModels() == ["openai/gpt-5.4", "openai/gpt-5.4-pro"])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1012,12 +1183,17 @@ extension TestChatTransportState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try await waitUntil("late model completion updates only the original session") {
|
try await waitUntil("late model completion updates only the original session") {
|
||||||
await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.model == "openai/gpt-5.4" }
|
await MainActor.run {
|
||||||
|
vm.sessions.first(where: { $0.key == "main" })?.model == "gpt-5.4" &&
|
||||||
|
vm.sessions.first(where: { $0.key == "main" })?.modelProvider == "openai"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#expect(await MainActor.run { vm.modelSelectionID } == "openai/gpt-5.4")
|
#expect(await MainActor.run { vm.modelSelectionID } == "openai/gpt-5.4")
|
||||||
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.model } == "openai/gpt-5.4")
|
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.model } == "gpt-5.4")
|
||||||
|
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.modelProvider } == "openai")
|
||||||
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "other" })?.model } == "openai/gpt-5.4-pro")
|
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "other" })?.model } == "openai/gpt-5.4-pro")
|
||||||
|
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "other" })?.modelProvider } == nil)
|
||||||
#expect(await transport.patchedModels() == ["openai/gpt-5.4", "openai/gpt-5.4-pro"])
|
#expect(await transport.patchedModels() == ["openai/gpt-5.4", "openai/gpt-5.4-pro"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ describe("runCronIsolatedAgentTurn", () => {
|
|||||||
setupIsolatedAgentTurnMocks();
|
setupIsolatedAgentTurnMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("delivers explicit targets directly", async () => {
|
it("delivers explicit targets with direct text", async () => {
|
||||||
await withTelegramAnnounceFixture(async ({ home, storePath, deps }) => {
|
await withTelegramAnnounceFixture(async ({ home, storePath, deps }) => {
|
||||||
await assertExplicitTelegramTargetDelivery({
|
await assertExplicitTelegramTargetDelivery({
|
||||||
home,
|
home,
|
||||||
@@ -209,7 +209,7 @@ describe("runCronIsolatedAgentTurn", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("delivers explicit targets with final payload text", async () => {
|
it("delivers explicit targets with final-payload text", async () => {
|
||||||
await withTelegramAnnounceFixture(async ({ home, storePath, deps }) => {
|
await withTelegramAnnounceFixture(async ({ home, storePath, deps }) => {
|
||||||
await assertExplicitTelegramTargetDelivery({
|
await assertExplicitTelegramTargetDelivery({
|
||||||
home,
|
home,
|
||||||
|
|||||||
@@ -17,10 +17,14 @@ vi.mock("./message.js", () => ({
|
|||||||
sendPoll: mocks.sendPoll,
|
sendPoll: mocks.sendPoll,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../media/local-roots.js", () => ({
|
vi.mock("../../media/local-roots.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../../media/local-roots.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
getDefaultMediaLocalRoots: mocks.getDefaultMediaLocalRoots,
|
getDefaultMediaLocalRoots: mocks.getDefaultMediaLocalRoots,
|
||||||
getAgentScopedMediaLocalRoots: mocks.getAgentScopedMediaLocalRoots,
|
getAgentScopedMediaLocalRoots: mocks.getAgentScopedMediaLocalRoots,
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
|
|
||||||
import { executePollAction, executeSendAction } from "./outbound-send-service.js";
|
import { executePollAction, executeSendAction } from "./outbound-send-service.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import * as authModule from "../agents/model-auth.js";
|
import * as authModule from "../agents/model-auth.js";
|
||||||
|
import * as ssrf from "../infra/net/ssrf.js";
|
||||||
import { type FetchMock, withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
import { type FetchMock, withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||||
import { createVoyageEmbeddingProvider, normalizeVoyageModel } from "./embeddings-voyage.js";
|
import { createVoyageEmbeddingProvider, normalizeVoyageModel } from "./embeddings-voyage.js";
|
||||||
|
|
||||||
@@ -27,6 +28,18 @@ function mockVoyageApiKey() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mockPublicPinnedHostname() {
|
||||||
|
return vi.spyOn(ssrf, "resolvePinnedHostnameWithPolicy").mockImplementation(async (hostname) => {
|
||||||
|
const normalized = hostname.trim().toLowerCase().replace(/\.$/, "");
|
||||||
|
const addresses = ["93.184.216.34"];
|
||||||
|
return {
|
||||||
|
hostname: normalized,
|
||||||
|
addresses,
|
||||||
|
lookup: ssrf.createPinnedLookup({ hostname: normalized, addresses }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function createDefaultVoyageProvider(
|
async function createDefaultVoyageProvider(
|
||||||
model: string,
|
model: string,
|
||||||
fetchMock: ReturnType<typeof createFetchMock>,
|
fetchMock: ReturnType<typeof createFetchMock>,
|
||||||
@@ -77,6 +90,7 @@ describe("voyage embedding provider", () => {
|
|||||||
it("respects remote overrides for baseUrl and apiKey", async () => {
|
it("respects remote overrides for baseUrl and apiKey", async () => {
|
||||||
const fetchMock = createFetchMock();
|
const fetchMock = createFetchMock();
|
||||||
vi.stubGlobal("fetch", fetchMock);
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
mockPublicPinnedHostname();
|
||||||
|
|
||||||
const result = await createVoyageEmbeddingProvider({
|
const result = await createVoyageEmbeddingProvider({
|
||||||
config: {} as never,
|
config: {} as never,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import * as authModule from "../agents/model-auth.js";
|
import * as authModule from "../agents/model-auth.js";
|
||||||
|
import * as ssrf from "../infra/net/ssrf.js";
|
||||||
import { DEFAULT_GEMINI_EMBEDDING_MODEL } from "./embeddings-gemini.js";
|
import { DEFAULT_GEMINI_EMBEDDING_MODEL } from "./embeddings-gemini.js";
|
||||||
import { createEmbeddingProvider, DEFAULT_LOCAL_MODEL } from "./embeddings.js";
|
import { createEmbeddingProvider, DEFAULT_LOCAL_MODEL } from "./embeddings.js";
|
||||||
|
|
||||||
@@ -32,6 +33,18 @@ function readFirstFetchRequest(fetchMock: { mock: { calls: unknown[][] } }) {
|
|||||||
return { url, init: init as RequestInit | undefined };
|
return { url, init: init as RequestInit | undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mockPublicPinnedHostname() {
|
||||||
|
return vi.spyOn(ssrf, "resolvePinnedHostnameWithPolicy").mockImplementation(async (hostname) => {
|
||||||
|
const normalized = hostname.trim().toLowerCase().replace(/\.$/, "");
|
||||||
|
const addresses = ["93.184.216.34"];
|
||||||
|
return {
|
||||||
|
hostname: normalized,
|
||||||
|
addresses,
|
||||||
|
lookup: ssrf.createPinnedLookup({ hostname: normalized, addresses }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
vi.unstubAllGlobals();
|
vi.unstubAllGlobals();
|
||||||
@@ -92,6 +105,7 @@ describe("embedding provider remote overrides", () => {
|
|||||||
it("uses remote baseUrl/apiKey and merges headers", async () => {
|
it("uses remote baseUrl/apiKey and merges headers", async () => {
|
||||||
const fetchMock = createFetchMock();
|
const fetchMock = createFetchMock();
|
||||||
vi.stubGlobal("fetch", fetchMock);
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
mockPublicPinnedHostname();
|
||||||
mockResolvedProviderKey("provider-key");
|
mockResolvedProviderKey("provider-key");
|
||||||
|
|
||||||
const cfg = {
|
const cfg = {
|
||||||
@@ -141,6 +155,7 @@ describe("embedding provider remote overrides", () => {
|
|||||||
it("falls back to resolved api key when remote apiKey is blank", async () => {
|
it("falls back to resolved api key when remote apiKey is blank", async () => {
|
||||||
const fetchMock = createFetchMock();
|
const fetchMock = createFetchMock();
|
||||||
vi.stubGlobal("fetch", fetchMock);
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
mockPublicPinnedHostname();
|
||||||
mockResolvedProviderKey("provider-key");
|
mockResolvedProviderKey("provider-key");
|
||||||
|
|
||||||
const cfg = {
|
const cfg = {
|
||||||
|
|||||||
Reference in New Issue
Block a user