mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
fix(apple): honor authoritative session routing
This commit is contained in:
@@ -405,6 +405,8 @@ final class NodeAppModel {
|
||||
|
||||
private var mainSessionBaseKey: String = "main"
|
||||
private var gatewaySessionScope: String?
|
||||
private var gatewaySessionRoutingContract: String?
|
||||
private var gatewayAgentSelectionRequired = false
|
||||
private var focusedChatSessionKey: String?
|
||||
// Two-part unread guard mirroring Android: the opened key survives read
|
||||
// confirmations so later unread episodes on the same open chat re-acknowledge;
|
||||
@@ -728,6 +730,8 @@ final class NodeAppModel {
|
||||
self.gatewaySessionScope = identity.scope
|
||||
self.mainSessionBaseKey = identity.mainSessionKey
|
||||
self.gatewayDefaultAgentId = identity.defaultAgentID
|
||||
self.gatewayAgentSelectionRequired = identity.selectionRequired
|
||||
self.gatewaySessionRoutingContract = identity.contract
|
||||
self.synchronizeTalkSessionKey()
|
||||
}
|
||||
|
||||
@@ -1604,7 +1608,9 @@ final class NodeAppModel {
|
||||
let routingIdentity = OpenClawChatSessionRoutingIdentity(
|
||||
scope: decoded.scope.value as? String,
|
||||
mainSessionKey: decoded.mainkey,
|
||||
defaultAgentID: decoded.defaultid)
|
||||
defaultAgentID: decoded.defaultid,
|
||||
selectionRequired: decoded.selectionrequired ?? false,
|
||||
sessionRoutingContract: decoded.sessionroutingcontract)
|
||||
guard shouldApply(),
|
||||
GatewayStableIdentifier.matches(self.chatTranscriptCacheGatewayID, sourceGatewayID)
|
||||
else { return }
|
||||
@@ -1612,6 +1618,8 @@ final class NodeAppModel {
|
||||
self.gatewayDefaultAgentId = decoded.defaultid
|
||||
self.gatewayAgents = decoded.agents
|
||||
self.gatewaySessionScope = decoded.scope.value as? String
|
||||
self.gatewayAgentSelectionRequired = decoded.selectionrequired ?? false
|
||||
self.gatewaySessionRoutingContract = routingIdentity?.contract
|
||||
self.applyMainSessionKey(decoded.mainkey)
|
||||
|
||||
let selected = (self.selectedAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
@@ -3447,12 +3455,13 @@ extension NodeAppModel {
|
||||
}
|
||||
let selected = (self.selectedAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !selected.isEmpty { return selected.lowercased() }
|
||||
guard !self.gatewayAgentSelectionRequired else { return nil }
|
||||
let defaultId = (self.gatewayDefaultAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return defaultId.isEmpty ? nil : defaultId.lowercased()
|
||||
}
|
||||
|
||||
var chatSessionRoutingContract: String? {
|
||||
OpenClawChatSessionRoutingContract.make(
|
||||
self.gatewaySessionRoutingContract ?? OpenClawChatSessionRoutingContract.make(
|
||||
scope: self.gatewaySessionScope,
|
||||
mainKey: self.mainSessionBaseKey,
|
||||
defaultAgentID: self.gatewayDefaultAgentId)
|
||||
@@ -3843,6 +3852,8 @@ extension NodeAppModel {
|
||||
LiveActivityManager.shared.endActivity(reason: "new_gateway_connect")
|
||||
self.mainSessionBaseKey = "main"
|
||||
self.gatewaySessionScope = nil
|
||||
self.gatewaySessionRoutingContract = nil
|
||||
self.gatewayAgentSelectionRequired = false
|
||||
self.gatewayDefaultAgentId = nil
|
||||
self.gatewayAgents = []
|
||||
self.selectedAgentId = GatewaySettingsStore.loadGatewaySelectedAgentId(stableID: stableID)
|
||||
@@ -5108,6 +5119,8 @@ extension NodeAppModel {
|
||||
private func configureLocalGatewayFixtureSession(agents: [AgentSummary]) {
|
||||
self.mainSessionBaseKey = "main"
|
||||
self.gatewaySessionScope = "per-sender"
|
||||
self.gatewaySessionRoutingContract = "per-sender|main|main"
|
||||
self.gatewayAgentSelectionRequired = false
|
||||
self.selectedAgentId = nil
|
||||
self.gatewayDefaultAgentId = "main"
|
||||
self.gatewayAgents = agents
|
||||
|
||||
@@ -2453,7 +2453,9 @@ private func overrideNotificationServingPreference(_ enabled: Bool) -> () -> Voi
|
||||
let identity = try #require(OpenClawChatSessionRoutingIdentity(
|
||||
scope: "per-sender",
|
||||
mainSessionKey: "restored-main",
|
||||
defaultAgentID: "main"))
|
||||
defaultAgentID: "main",
|
||||
selectionRequired: true,
|
||||
sessionRoutingContract: "per-sender|restored-main|unowned"))
|
||||
let store = databases.store(gatewayID: stableID)
|
||||
await store.storeSessionRoutingIdentity(identity)
|
||||
await store.retire()
|
||||
@@ -2480,6 +2482,7 @@ private func overrideNotificationServingPreference(_ enabled: Bool) -> () -> Voi
|
||||
|
||||
#expect(talkMode.isGatewayConnected)
|
||||
#expect(appModel.chatSessionRoutingContract == identity.contract)
|
||||
#expect(appModel.chatDeliveryAgentId == nil)
|
||||
#expect(talkMode.isUsingMainSessionKey(appModel.chatSessionKey))
|
||||
}
|
||||
|
||||
|
||||
@@ -1053,7 +1053,8 @@ final class WebChatSwiftUIWindowController: NSObject, NSWindowDelegate {
|
||||
let explicitAgentID = WebChatRoute.normalizedAgentID(agentID)
|
||||
let effectiveAgentID = Self.effectiveAgentID(
|
||||
explicitAgentID: explicitAgentID,
|
||||
cachedDefaultAgentID: cachedRoutingIdentity?.defaultAgentID)
|
||||
cachedDefaultAgentID: cachedRoutingIdentity?.defaultAgentID,
|
||||
selectionRequired: cachedRoutingIdentity?.selectionRequired ?? false)
|
||||
self.init(
|
||||
sessionKey: sessionKey,
|
||||
initialDraft: initialDraft,
|
||||
@@ -1148,15 +1149,15 @@ final class WebChatSwiftUIWindowController: NSObject, NSWindowDelegate {
|
||||
// default refreshes only supply the fallback route.
|
||||
let effectiveAgentID = Self.effectiveAgentID(
|
||||
explicitAgentID: explicitAgentID,
|
||||
cachedDefaultAgentID: routingIdentity.defaultAgentID)
|
||||
cachedDefaultAgentID: routingIdentity.defaultAgentID,
|
||||
selectionRequired: routingIdentity.selectionRequired)
|
||||
(transport as? MacGatewayChatTransport)?
|
||||
.updateDefaultGlobalAgentID(effectiveAgentID)
|
||||
if let store = transcriptCache as? OpenClawChatSQLiteTranscriptCache,
|
||||
!usesPrimaryAppRuntime || store.gatewayID == MacChatTranscriptCache.currentGatewayID(),
|
||||
let persistedIdentity = OpenClawChatSessionRoutingIdentity(
|
||||
contract: routingIdentity.contract)
|
||||
!usesPrimaryAppRuntime || store.gatewayID == MacChatTranscriptCache.currentGatewayID()
|
||||
{
|
||||
await store.storeSessionRoutingIdentity(persistedIdentity)
|
||||
// Persist the display default separately from the authoritative contract.
|
||||
await store.storeSessionRoutingIdentity(routingIdentity)
|
||||
}
|
||||
vm.syncDeliveryIdentity(
|
||||
activeAgentId: effectiveAgentID,
|
||||
@@ -1254,10 +1255,14 @@ final class WebChatSwiftUIWindowController: NSObject, NSWindowDelegate {
|
||||
|
||||
static func effectiveAgentID(
|
||||
explicitAgentID: String?,
|
||||
cachedDefaultAgentID: String?) -> String?
|
||||
cachedDefaultAgentID: String?,
|
||||
selectionRequired: Bool = false) -> String?
|
||||
{
|
||||
WebChatRoute.normalizedAgentID(explicitAgentID)
|
||||
?? WebChatRoute.normalizedAgentID(cachedDefaultAgentID)
|
||||
if let explicitAgentID = WebChatRoute.normalizedAgentID(explicitAgentID) {
|
||||
return explicitAgentID
|
||||
}
|
||||
guard !selectionRequired else { return nil }
|
||||
return WebChatRoute.normalizedAgentID(cachedDefaultAgentID)
|
||||
}
|
||||
|
||||
private static func makeWindow(
|
||||
|
||||
@@ -668,11 +668,12 @@ private func assertConfigLookupCannotRecreateRoute(
|
||||
}
|
||||
|
||||
@Test func `routing identity decodes agent and contract from one response`() throws {
|
||||
let data = Data(#"{"defaultId":"Work","mainKey":"Primary","scope":"global","agents":[]}"#.utf8)
|
||||
let data = Data(#"{"defaultId":"Work","mainKey":"Primary","scope":"per-sender","selectionRequired":true,"sessionRoutingContract":"per-sender|Primary|unowned","agents":[]}"#.utf8)
|
||||
let identity = try OpenClawChatGatewayPayloadCodec.decodeSessionRoutingIdentity(data)
|
||||
|
||||
#expect(identity.defaultAgentID == "work")
|
||||
#expect(identity.contract == "global|primary|work")
|
||||
#expect(identity.selectionRequired)
|
||||
#expect(identity.contract == "per-sender|Primary|unowned")
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
|
||||
@@ -219,6 +219,30 @@ struct WebChatSwiftUISmokeTests {
|
||||
fallback.close()
|
||||
}
|
||||
|
||||
@Test func `controller refuses cached display default when gateway requires selection`() throws {
|
||||
let cachedIdentity = try #require(OpenClawChatSessionRoutingIdentity(
|
||||
scope: "per-sender",
|
||||
mainSessionKey: "main",
|
||||
defaultAgentID: "main",
|
||||
selectionRequired: true,
|
||||
sessionRoutingContract: "per-sender|main|unowned"))
|
||||
let unresolved = WebChatSwiftUIWindowController(
|
||||
sessionKey: "main",
|
||||
agentID: nil,
|
||||
cachedRoutingIdentity: cachedIdentity,
|
||||
store: nil)
|
||||
let explicit = WebChatSwiftUIWindowController(
|
||||
sessionKey: "main",
|
||||
agentID: " Work ",
|
||||
cachedRoutingIdentity: cachedIdentity,
|
||||
store: nil)
|
||||
|
||||
#expect(unresolved._testActiveAgentID == nil)
|
||||
#expect(explicit._testActiveAgentID == "work")
|
||||
unresolved.close()
|
||||
explicit.close()
|
||||
}
|
||||
|
||||
@Test func `max and Ultra thinking preferences survive reopen`() throws {
|
||||
let suiteName = "WebChatSwiftUISmokeTests.\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
||||
|
||||
@@ -53,7 +53,9 @@ public enum OpenClawChatGatewayPayloadCodec {
|
||||
guard let identity = OpenClawChatSessionRoutingIdentity(
|
||||
scope: decoded.scope.value as? String,
|
||||
mainSessionKey: decoded.mainkey,
|
||||
defaultAgentID: decoded.defaultid)
|
||||
defaultAgentID: decoded.defaultid,
|
||||
selectionRequired: decoded.selectionrequired ?? false,
|
||||
sessionRoutingContract: decoded.sessionroutingcontract)
|
||||
else { throw CancellationError() }
|
||||
return identity
|
||||
}
|
||||
|
||||
@@ -328,15 +328,19 @@ public actor OpenClawChatSQLiteTranscriptCache: OpenClawChatTranscriptCache,
|
||||
let gatewayID = self.gatewayID
|
||||
do {
|
||||
try await self.databases.stateQueue.write { db in
|
||||
try OpenClawClientDatabases.ensureSessionRoutingIdentityColumns(in: db)
|
||||
try db.execute(
|
||||
sql: """
|
||||
INSERT INTO gateway_routing_identity(
|
||||
gateway_id, scope, main_session_key, default_agent_id, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?)
|
||||
gateway_id, scope, main_session_key, default_agent_id,
|
||||
routing_contract, selection_required, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(gateway_id) DO UPDATE SET
|
||||
scope = excluded.scope,
|
||||
main_session_key = excluded.main_session_key,
|
||||
default_agent_id = excluded.default_agent_id,
|
||||
routing_contract = excluded.routing_contract,
|
||||
selection_required = excluded.selection_required,
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
arguments: [
|
||||
@@ -344,6 +348,8 @@ public actor OpenClawChatSQLiteTranscriptCache: OpenClawChatTranscriptCache,
|
||||
identity.scope,
|
||||
identity.mainSessionKey,
|
||||
identity.defaultAgentID,
|
||||
identity.contract,
|
||||
identity.selectionRequired ? 1 : 0,
|
||||
Date().timeIntervalSince1970,
|
||||
])
|
||||
}
|
||||
|
||||
@@ -410,6 +410,7 @@ public struct OpenClawChatSessionRoutingIdentity: Equatable, Sendable {
|
||||
public let scope: String
|
||||
public let mainSessionKey: String
|
||||
public let defaultAgentID: String
|
||||
public let selectionRequired: Bool
|
||||
public let contract: String
|
||||
|
||||
public init?(contract: String?) {
|
||||
@@ -417,15 +418,42 @@ public struct OpenClawChatSessionRoutingIdentity: Equatable, Sendable {
|
||||
self.scope = components.scope
|
||||
self.mainSessionKey = components.mainKey
|
||||
self.defaultAgentID = components.defaultAgentID
|
||||
self.selectionRequired = false
|
||||
self.contract = "\(components.scope)|\(components.mainKey)|\(components.defaultAgentID)"
|
||||
}
|
||||
|
||||
public init?(scope: String?, mainSessionKey: String?, defaultAgentID: String?) {
|
||||
guard let contract = OpenClawChatSessionRoutingContract.make(
|
||||
self.init(
|
||||
scope: scope,
|
||||
mainSessionKey: mainSessionKey,
|
||||
defaultAgentID: defaultAgentID,
|
||||
selectionRequired: false,
|
||||
sessionRoutingContract: nil)
|
||||
}
|
||||
|
||||
public init?(
|
||||
scope: String?,
|
||||
mainSessionKey: String?,
|
||||
defaultAgentID: String?,
|
||||
selectionRequired: Bool,
|
||||
sessionRoutingContract: String?)
|
||||
{
|
||||
guard let displayContract = OpenClawChatSessionRoutingContract.make(
|
||||
scope: scope,
|
||||
mainKey: mainSessionKey,
|
||||
defaultAgentID: defaultAgentID)
|
||||
defaultAgentID: defaultAgentID),
|
||||
let display = OpenClawChatSessionRoutingContract.parse(displayContract)
|
||||
else { return nil }
|
||||
self.init(contract: contract)
|
||||
let authoritativeContract = sessionRoutingContract?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.scope = display.scope
|
||||
self.mainSessionKey = display.mainKey
|
||||
self.defaultAgentID = display.defaultAgentID
|
||||
self.selectionRequired = selectionRequired
|
||||
if let authoritativeContract, !authoritativeContract.isEmpty {
|
||||
self.contract = authoritativeContract
|
||||
} else {
|
||||
self.contract = displayContract
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,11 +76,13 @@ public final class OpenClawClientDatabases: @unchecked Sendable {
|
||||
gatewayID: String) -> OpenClawChatSessionRoutingIdentity?
|
||||
{
|
||||
do {
|
||||
return try self.stateQueue.read { db in
|
||||
return try self.stateQueue.write { db in
|
||||
try Self.ensureSessionRoutingIdentityColumns(in: db)
|
||||
guard let row = try Row.fetchOne(
|
||||
db,
|
||||
sql: """
|
||||
SELECT scope, main_session_key, default_agent_id
|
||||
SELECT scope, main_session_key, default_agent_id,
|
||||
selection_required, routing_contract
|
||||
FROM gateway_routing_identity WHERE gateway_id = ?
|
||||
""",
|
||||
arguments: [gatewayID])
|
||||
@@ -88,7 +90,9 @@ public final class OpenClawClientDatabases: @unchecked Sendable {
|
||||
return OpenClawChatSessionRoutingIdentity(
|
||||
scope: row["scope"],
|
||||
mainSessionKey: row["main_session_key"],
|
||||
defaultAgentID: row["default_agent_id"])
|
||||
defaultAgentID: row["default_agent_id"],
|
||||
selectionRequired: (row["selection_required"] as Int?) == 1,
|
||||
sessionRoutingContract: row["routing_contract"])
|
||||
}
|
||||
} catch {
|
||||
databaseLogger.error("client state routing read failed: \(error.localizedDescription, privacy: .public)")
|
||||
@@ -447,6 +451,8 @@ extension OpenClawClientDatabases {
|
||||
scope TEXT NOT NULL,
|
||||
main_session_key TEXT NOT NULL,
|
||||
default_agent_id TEXT NOT NULL,
|
||||
routing_contract TEXT,
|
||||
selection_required INTEGER,
|
||||
updated_at REAL NOT NULL
|
||||
);
|
||||
CREATE TABLE outbox_commands(
|
||||
@@ -565,6 +571,16 @@ extension OpenClawClientDatabases {
|
||||
return queue
|
||||
}
|
||||
|
||||
static func ensureSessionRoutingIdentityColumns(in db: Database) throws {
|
||||
let columns = try Set(db.columns(in: "gateway_routing_identity").map(\.name))
|
||||
if !columns.contains("routing_contract") {
|
||||
try db.execute(sql: "ALTER TABLE gateway_routing_identity ADD COLUMN routing_contract TEXT")
|
||||
}
|
||||
if !columns.contains("selection_required") {
|
||||
try db.execute(sql: "ALTER TABLE gateway_routing_identity ADD COLUMN selection_required INTEGER")
|
||||
}
|
||||
}
|
||||
|
||||
private static func openRepairableCacheDatabase(at url: URL) throws -> DatabaseQueue {
|
||||
do {
|
||||
let queue = try DatabaseQueue(
|
||||
@@ -674,6 +690,8 @@ extension OpenClawClientDatabases {
|
||||
var scope: String
|
||||
var mainSessionKey: String
|
||||
var defaultAgentID: String
|
||||
var routingContract: String?
|
||||
var selectionRequired: Bool
|
||||
var updatedAt: Double
|
||||
}
|
||||
|
||||
@@ -806,8 +824,15 @@ extension OpenClawClientDatabases {
|
||||
}
|
||||
}
|
||||
if try db.tableExists("gateway_routing_identity") {
|
||||
let columns = try Set(db.columns(in: "gateway_routing_identity").map(\.name))
|
||||
func expression(_ name: String, fallback: String) -> String {
|
||||
columns.contains(name) ? name : fallback
|
||||
}
|
||||
let rows = try Row.fetchAll(db, sql: """
|
||||
SELECT gateway_id, scope, main_session_key, default_agent_id, updated_at
|
||||
SELECT gateway_id, scope, main_session_key, default_agent_id,
|
||||
\(expression("routing_contract", fallback: "NULL")) AS routing_contract,
|
||||
\(expression("selection_required", fallback: "0")) AS selection_required,
|
||||
updated_at
|
||||
FROM gateway_routing_identity
|
||||
""")
|
||||
snapshot.routingIdentities = rows.map { row in
|
||||
@@ -816,6 +841,8 @@ extension OpenClawClientDatabases {
|
||||
scope: row["scope"],
|
||||
mainSessionKey: row["main_session_key"],
|
||||
defaultAgentID: row["default_agent_id"],
|
||||
routingContract: row["routing_contract"],
|
||||
selectionRequired: (row["selection_required"] as Int) == 1,
|
||||
updatedAt: row["updated_at"])
|
||||
}
|
||||
}
|
||||
@@ -825,6 +852,7 @@ extension OpenClawClientDatabases {
|
||||
|
||||
private func writeLegacySnapshot(_ snapshot: LegacySnapshot) throws {
|
||||
try self.stateQueue.write { db in
|
||||
try Self.ensureSessionRoutingIdentityColumns(in: db)
|
||||
let forgottenGatewayHashes = try Set(String.fetchAll(
|
||||
db,
|
||||
sql: """
|
||||
@@ -837,12 +865,15 @@ extension OpenClawClientDatabases {
|
||||
try db.execute(
|
||||
sql: """
|
||||
INSERT INTO gateway_routing_identity(
|
||||
gateway_id, scope, main_session_key, default_agent_id, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?)
|
||||
gateway_id, scope, main_session_key, default_agent_id,
|
||||
routing_contract, selection_required, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(gateway_id) DO UPDATE SET
|
||||
scope = excluded.scope,
|
||||
main_session_key = excluded.main_session_key,
|
||||
default_agent_id = excluded.default_agent_id,
|
||||
routing_contract = excluded.routing_contract,
|
||||
selection_required = excluded.selection_required,
|
||||
updated_at = excluded.updated_at
|
||||
WHERE excluded.updated_at > gateway_routing_identity.updated_at
|
||||
""",
|
||||
@@ -851,6 +882,8 @@ extension OpenClawClientDatabases {
|
||||
identity.scope,
|
||||
identity.mainSessionKey,
|
||||
identity.defaultAgentID,
|
||||
identity.routingContract,
|
||||
identity.selectionRequired ? 1 : 0,
|
||||
identity.updatedAt,
|
||||
])
|
||||
}
|
||||
|
||||
@@ -412,10 +412,20 @@ struct ChatGatewayPayloadCodecTests {
|
||||
}
|
||||
|
||||
@Test func `routing identity decodes agent and canonical contract`() throws {
|
||||
let identity = try OpenClawChatGatewayPayloadCodec.decodeSessionRoutingIdentity(
|
||||
Data(#"{"defaultId":"Work","mainKey":"Primary","scope":"per-sender","selectionRequired":true,"sessionRoutingContract":"per-sender|Primary|unowned","agents":[]}"#.utf8))
|
||||
|
||||
#expect(identity.defaultAgentID == "work")
|
||||
#expect(identity.selectionRequired)
|
||||
#expect(identity.contract == "per-sender|Primary|unowned")
|
||||
}
|
||||
|
||||
@Test func `routing identity reconstructs legacy gateway contract`() throws {
|
||||
let identity = try OpenClawChatGatewayPayloadCodec.decodeSessionRoutingIdentity(
|
||||
Data(#"{"defaultId":"Work","mainKey":"Primary","scope":"global","agents":[]}"#.utf8))
|
||||
|
||||
#expect(identity.defaultAgentID == "work")
|
||||
#expect(!identity.selectionRequired)
|
||||
#expect(identity.contract == "global|primary|work")
|
||||
}
|
||||
|
||||
|
||||
@@ -730,13 +730,16 @@ final class ChatTranscriptCacheStoreTests: ClientDatabaseTestSuite, @unchecked S
|
||||
let identity = try #require(OpenClawChatSessionRoutingIdentity(
|
||||
scope: " Per-Sender ",
|
||||
mainSessionKey: " Work ",
|
||||
defaultAgentID: " Main "))
|
||||
defaultAgentID: " Main ",
|
||||
selectionRequired: true,
|
||||
sessionRoutingContract: "per-sender|work|unowned"))
|
||||
await databases.store(gatewayID: "gw-a").storeSessionRoutingIdentity(identity)
|
||||
try databases.close()
|
||||
|
||||
let reopened = try OpenClawClientDatabases(directoryURL: directory)
|
||||
#expect(reopened.loadSessionRoutingIdentity(gatewayID: "gw-a") == identity)
|
||||
#expect(identity.contract == "per-sender|work|main")
|
||||
#expect(identity.contract == "per-sender|work|unowned")
|
||||
#expect(identity.selectionRequired)
|
||||
#expect(try await reopened.stateQueue.read { db in
|
||||
try String.fetchAll(db, sql: "SELECT identifier FROM grdb_migrations")
|
||||
} == [
|
||||
@@ -748,6 +751,39 @@ final class ChatTranscriptCacheStoreTests: ClientDatabaseTestSuite, @unchecked S
|
||||
"client-state-outbox-attachment-rekey-v6",
|
||||
])
|
||||
}
|
||||
|
||||
@Test func `routing identity lazily upgrades the legacy table shape`() async throws {
|
||||
try databases.close()
|
||||
let stateURL = directory.appendingPathComponent("client-state.sqlite")
|
||||
try withRawDatabase(at: stateURL) { raw in
|
||||
execute(raw, "ALTER TABLE gateway_routing_identity RENAME TO gateway_routing_identity_new")
|
||||
execute(raw, """
|
||||
CREATE TABLE gateway_routing_identity(
|
||||
gateway_id TEXT NOT NULL PRIMARY KEY,
|
||||
scope TEXT NOT NULL,
|
||||
main_session_key TEXT NOT NULL,
|
||||
default_agent_id TEXT NOT NULL,
|
||||
updated_at REAL NOT NULL
|
||||
)
|
||||
""")
|
||||
execute(raw, "DROP TABLE gateway_routing_identity_new")
|
||||
}
|
||||
let reopened = try OpenClawClientDatabases(directoryURL: directory)
|
||||
let identity = try #require(OpenClawChatSessionRoutingIdentity(
|
||||
scope: "per-sender",
|
||||
mainSessionKey: "main",
|
||||
defaultAgentID: "main",
|
||||
selectionRequired: true,
|
||||
sessionRoutingContract: "per-sender|main|unowned"))
|
||||
|
||||
await reopened.store(gatewayID: "gw-a").storeSessionRoutingIdentity(identity)
|
||||
|
||||
#expect(reopened.loadSessionRoutingIdentity(gatewayID: "gw-a") == identity)
|
||||
#expect(try await reopened.stateQueue.read { db in
|
||||
Set(try db.columns(in: "gateway_routing_identity").map(\.name))
|
||||
.isSuperset(of: ["routing_contract", "selection_required"])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
final class ClientDatabaseLegacyImportTests: TemporaryDatabaseTestSuite, @unchecked Sendable {
|
||||
|
||||
Reference in New Issue
Block a user