



























@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
1515import kotlinx.coroutines.runBlocking
1616import org.junit.Assert.assertEquals
1717import org.junit.Assert.assertFalse
18+import org.junit.Assert.assertNotNull
1819import org.junit.Assert.assertNull
1920import org.junit.Assert.assertTrue
2021import org.junit.Test
@@ -120,6 +121,31 @@ class GatewayBootstrapAuthTest {
120121 )
121122 }
122123124+ @Test
125+fun nodeConnectStartsOperatorAfterBootstrapHandoffWhenOperatorWasConnecting() {
126+val app = RuntimeEnvironment.getApplication()
127+val securePrefs =
128+ app.getSharedPreferences(
129+"openclaw.node.secure.test.${UUID.randomUUID()}",
130+ android.content.Context.MODE_PRIVATE,
131+ )
132+val prefs = SecurePrefs(app, securePrefsOverride = securePrefs)
133+val runtime = NodeRuntime(app, prefs)
134+val deviceId = DeviceIdentityStore(app).loadOrCreate().deviceId
135+DeviceAuthStore(prefs).saveToken(deviceId, "operator", "bootstrap-operator-token")
136+137+ writeField(runtime, "operatorStatusText", "Connecting…")
138+ invokeMaybeStartOperatorSessionAfterNodeConnect(
139+ runtime = runtime,
140+ endpoint = GatewayEndpoint.manual(host = "127.0.0.1", port = 18789),
141+ auth = NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "setup-bootstrap-token", password = null),
142+ )
143+144+val desired = desiredConnection(runtime, "operatorSession")
145+ assertNotNull(desired)
146+ assertNull(readField<String?>(desired!!, "bootstrapToken"))
147+ }
148+123149 @Test
124150fun resolveGatewayConnectAuth_prefersExplicitSetupAuthOverStoredPrefs() {
125151val app = RuntimeEnvironment.getApplication()
@@ -358,11 +384,52 @@ class GatewayBootstrapAuthTest {
358384runtime: NodeRuntime,
359385sessionFieldName: String,
360386 ): String? {
361-val session = readField<GatewaySession>(runtime, sessionFieldName)
362-val desired = readField<Any?>(session, "desired") ?: return null
387+val desired = desiredConnection(runtime, sessionFieldName) ?: return null
363388return readField(desired, "bootstrapToken")
364389 }
365390391+private fun desiredConnection(
392+runtime: NodeRuntime,
393+sessionFieldName: String,
394+ ): Any? {
395+val session = readField<GatewaySession>(runtime, sessionFieldName)
396+return readField(session, "desired")
397+ }
398+399+private fun invokeMaybeStartOperatorSessionAfterNodeConnect(
400+runtime: NodeRuntime,
401+endpoint: GatewayEndpoint,
402+auth: NodeRuntime.GatewayConnectAuth,
403+ ) {
404+val method =
405+ runtime.javaClass.getDeclaredMethod(
406+"maybeStartOperatorSessionAfterNodeConnect",
407+GatewayEndpoint::class.java,
408+NodeRuntime.GatewayConnectAuth::class.java,
409+ )
410+ method.isAccessible = true
411+ method.invoke(runtime, endpoint, auth)
412+ }
413+414+private fun writeField(
415+target: Any,
416+name: String,
417+value: Any?,
418+ ) {
419+var type: Class<*>? = target.javaClass
420+while (type != null) {
421+try {
422+val field: Field = type.getDeclaredField(name)
423+ field.isAccessible = true
424+ field.set(target, value)
425+return
426+ } catch (_: NoSuchFieldException) {
427+ type = type.superclass
428+ }
429+ }
430+ error("Field $name not found on ${target.javaClass.name}")
431+ }
432+366433private fun waitForDesiredBootstrapToken(
367434runtime: NodeRuntime,
368435sessionFieldName: String,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。