mirror of https://github.com/openclaw/openclaw.git
Merge branch 'main' into main
This commit is contained in:
commit
b33f56a29a
|
|
@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
|
||||||
- Telegram/media downloads: thread the same direct or proxy transport policy into SSRF-guarded file fetches so inbound attachments keep working when Telegram falls back between env-proxy and direct networking. (#44639) Thanks @obviyus.
|
- Telegram/media downloads: thread the same direct or proxy transport policy into SSRF-guarded file fetches so inbound attachments keep working when Telegram falls back between env-proxy and direct networking. (#44639) Thanks @obviyus.
|
||||||
- Agents/compaction: compare post-compaction token sanity checks against full-session pre-compaction totals and skip the check when token estimation fails, so sessions with large bootstrap context keep real token counts instead of falling back to unknown. (#28347) thanks @efe-arv.
|
- Agents/compaction: compare post-compaction token sanity checks against full-session pre-compaction totals and skip the check when token estimation fails, so sessions with large bootstrap context keep real token counts instead of falling back to unknown. (#28347) thanks @efe-arv.
|
||||||
- Discord/gateway startup: treat plain-text and transient `/gateway/bot` metadata fetch failures as transient startup errors so Discord gateway boot no longer crashes on unhandled rejections. (#44397) Thanks @jalehman.
|
- Discord/gateway startup: treat plain-text and transient `/gateway/bot` metadata fetch failures as transient startup errors so Discord gateway boot no longer crashes on unhandled rejections. (#44397) Thanks @jalehman.
|
||||||
|
- Gateway/session reset: preserve `lastAccountId` and `lastThreadId` across gateway session resets so replies keep routing back to the same account and thread after `/reset`. (#44773) Thanks @Lanfei.
|
||||||
|
|
||||||
## 2026.3.12
|
## 2026.3.12
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,16 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.ChatBubble
|
||||||
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
|
import androidx.compose.material.icons.filled.Cloud
|
||||||
import androidx.compose.material.icons.filled.ExpandLess
|
import androidx.compose.material.icons.filled.ExpandLess
|
||||||
import androidx.compose.material.icons.filled.ExpandMore
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
|
import androidx.compose.material.icons.filled.Link
|
||||||
|
import androidx.compose.material.icons.filled.Security
|
||||||
|
import androidx.compose.material.icons.filled.Tune
|
||||||
|
import androidx.compose.material.icons.filled.Wifi
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
|
@ -68,6 +76,7 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
@ -513,25 +522,20 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(top = 12.dp),
|
modifier = Modifier.padding(top = 12.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"FIRST RUN",
|
"OpenClaw",
|
||||||
style = onboardingCaption1Style.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.5.sp),
|
style = onboardingDisplayStyle,
|
||||||
color = onboardingAccent,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
"OpenClaw\nMobile Setup",
|
|
||||||
style = onboardingDisplayStyle.copy(lineHeight = 38.sp),
|
|
||||||
color = onboardingText,
|
color = onboardingText,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"Step ${step.index} of 4",
|
"Mobile Setup",
|
||||||
style = onboardingCaption1Style,
|
style = onboardingTitle1Style,
|
||||||
color = onboardingAccent,
|
color = onboardingTextSecondary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
StepRailWrap(current = step)
|
StepRail(current = step)
|
||||||
|
|
||||||
when (step) {
|
when (step) {
|
||||||
OnboardingStep.Welcome -> WelcomeStep()
|
OnboardingStep.Welcome -> WelcomeStep()
|
||||||
|
|
@ -892,15 +896,6 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun StepRailWrap(current: OnboardingStep) {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
|
||||||
HorizontalDivider(color = onboardingBorder)
|
|
||||||
StepRail(current = current)
|
|
||||||
HorizontalDivider(color = onboardingBorder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun StepRail(current: OnboardingStep) {
|
private fun StepRail(current: OnboardingStep) {
|
||||||
val steps = OnboardingStep.entries
|
val steps = OnboardingStep.entries
|
||||||
|
|
@ -942,11 +937,31 @@ private fun StepRail(current: OnboardingStep) {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun WelcomeStep() {
|
private fun WelcomeStep() {
|
||||||
StepShell(title = "What You Get") {
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
Bullet("Control the gateway and operator chat from one mobile surface.")
|
FeatureCard(
|
||||||
Bullet("Connect with setup code and recover pairing with CLI commands.")
|
icon = Icons.Default.Wifi,
|
||||||
Bullet("Enable only the permissions and capabilities you want.")
|
title = "Connect to your gateway",
|
||||||
Bullet("Finish with a real connection check before entering the app.")
|
subtitle = "Scan a QR code or enter your host manually",
|
||||||
|
accentColor = onboardingAccent,
|
||||||
|
)
|
||||||
|
FeatureCard(
|
||||||
|
icon = Icons.Default.Tune,
|
||||||
|
title = "Choose your permissions",
|
||||||
|
subtitle = "Enable only what you need, change anytime",
|
||||||
|
accentColor = Color(0xFF7C5AC7),
|
||||||
|
)
|
||||||
|
FeatureCard(
|
||||||
|
icon = Icons.Default.ChatBubble,
|
||||||
|
title = "Chat, voice, and screen",
|
||||||
|
subtitle = "Full operator control from your phone",
|
||||||
|
accentColor = onboardingSuccess,
|
||||||
|
)
|
||||||
|
FeatureCard(
|
||||||
|
icon = Icons.Default.CheckCircle,
|
||||||
|
title = "Verify your connection",
|
||||||
|
subtitle = "Live check before you enter the app",
|
||||||
|
accentColor = Color(0xFFC8841A),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -975,11 +990,12 @@ private fun GatewayStep(
|
||||||
val manualResolvedEndpoint = remember(manualHost, manualPort, manualTls) { composeGatewayManualUrl(manualHost, manualPort, manualTls)?.let { parseGatewayEndpoint(it)?.displayUrl } }
|
val manualResolvedEndpoint = remember(manualHost, manualPort, manualTls) { composeGatewayManualUrl(manualHost, manualPort, manualTls)?.let { parseGatewayEndpoint(it)?.displayUrl } }
|
||||||
|
|
||||||
StepShell(title = "Gateway Connection") {
|
StepShell(title = "Gateway Connection") {
|
||||||
GuideBlock(title = "Scan onboarding QR") {
|
Text(
|
||||||
Text("Run these on the gateway host:", style = onboardingCalloutStyle, color = onboardingTextSecondary)
|
"Run `openclaw qr` on your gateway host, then scan the code with this device.",
|
||||||
CommandBlock("openclaw qr")
|
style = onboardingCalloutStyle,
|
||||||
Text("Then scan with this device.", style = onboardingCalloutStyle, color = onboardingTextSecondary)
|
color = onboardingTextSecondary,
|
||||||
}
|
)
|
||||||
|
CommandBlock("openclaw qr")
|
||||||
Button(
|
Button(
|
||||||
onClick = onScanQrClick,
|
onClick = onScanQrClick,
|
||||||
modifier = Modifier.fillMaxWidth().height(48.dp),
|
modifier = Modifier.fillMaxWidth().height(48.dp),
|
||||||
|
|
@ -1023,21 +1039,6 @@ private fun GatewayStep(
|
||||||
|
|
||||||
AnimatedVisibility(visible = advancedOpen) {
|
AnimatedVisibility(visible = advancedOpen) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
GuideBlock(title = "Manual setup commands") {
|
|
||||||
Text("Run these on the gateway host:", style = onboardingCalloutStyle, color = onboardingTextSecondary)
|
|
||||||
CommandBlock("openclaw qr --setup-code-only")
|
|
||||||
CommandBlock("openclaw qr --json")
|
|
||||||
Text(
|
|
||||||
"`--json` prints `setupCode` and `gatewayUrl`.",
|
|
||||||
style = onboardingCalloutStyle,
|
|
||||||
color = onboardingTextSecondary,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
"Auto URL discovery is not wired yet. Android emulator uses `10.0.2.2`; real devices need LAN/Tailscale host.",
|
|
||||||
style = onboardingCalloutStyle,
|
|
||||||
color = onboardingTextSecondary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
GatewayModeToggle(inputMode = inputMode, onInputModeChange = onInputModeChange)
|
GatewayModeToggle(inputMode = inputMode, onInputModeChange = onInputModeChange)
|
||||||
|
|
||||||
if (inputMode == GatewayInputMode.SetupCode) {
|
if (inputMode == GatewayInputMode.SetupCode) {
|
||||||
|
|
@ -1306,13 +1307,9 @@ private fun StepShell(
|
||||||
title: String,
|
title: String,
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(0.dp)) {
|
Column(modifier = Modifier.padding(vertical = 4.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
HorizontalDivider(color = onboardingBorder)
|
Text(title, style = onboardingTitle1Style, color = onboardingText)
|
||||||
Column(modifier = Modifier.padding(vertical = 14.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
content()
|
||||||
Text(title, style = onboardingTitle1Style, color = onboardingText)
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
HorizontalDivider(color = onboardingBorder)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1378,13 +1375,15 @@ private fun PermissionsStep(
|
||||||
|
|
||||||
StepShell(title = "Permissions") {
|
StepShell(title = "Permissions") {
|
||||||
Text(
|
Text(
|
||||||
"Enable only what you need now. You can change everything later in Settings.",
|
"Enable only what you need. You can change these anytime in Settings.",
|
||||||
style = onboardingCalloutStyle,
|
style = onboardingCalloutStyle,
|
||||||
color = onboardingTextSecondary,
|
color = onboardingTextSecondary,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PermissionSectionHeader("System")
|
||||||
PermissionToggleRow(
|
PermissionToggleRow(
|
||||||
title = "Gateway discovery",
|
title = "Gateway discovery",
|
||||||
subtitle = if (Build.VERSION.SDK_INT >= 33) "Nearby devices" else "Location (for NSD)",
|
subtitle = "Find gateways on your local network",
|
||||||
checked = enableDiscovery,
|
checked = enableDiscovery,
|
||||||
granted = isPermissionGranted(context, discoveryPermission),
|
granted = isPermissionGranted(context, discoveryPermission),
|
||||||
onCheckedChange = onDiscoveryChange,
|
onCheckedChange = onDiscoveryChange,
|
||||||
|
|
@ -1392,7 +1391,7 @@ private fun PermissionsStep(
|
||||||
InlineDivider()
|
InlineDivider()
|
||||||
PermissionToggleRow(
|
PermissionToggleRow(
|
||||||
title = "Location",
|
title = "Location",
|
||||||
subtitle = "location.get (while app is open)",
|
subtitle = "Share device location while app is open",
|
||||||
checked = enableLocation,
|
checked = enableLocation,
|
||||||
granted = locationGranted,
|
granted = locationGranted,
|
||||||
onCheckedChange = onLocationChange,
|
onCheckedChange = onLocationChange,
|
||||||
|
|
@ -1401,7 +1400,7 @@ private fun PermissionsStep(
|
||||||
if (Build.VERSION.SDK_INT >= 33) {
|
if (Build.VERSION.SDK_INT >= 33) {
|
||||||
PermissionToggleRow(
|
PermissionToggleRow(
|
||||||
title = "Notifications",
|
title = "Notifications",
|
||||||
subtitle = "system.notify and foreground alerts",
|
subtitle = "Alerts and foreground service notices",
|
||||||
checked = enableNotifications,
|
checked = enableNotifications,
|
||||||
granted = isPermissionGranted(context, Manifest.permission.POST_NOTIFICATIONS),
|
granted = isPermissionGranted(context, Manifest.permission.POST_NOTIFICATIONS),
|
||||||
onCheckedChange = onNotificationsChange,
|
onCheckedChange = onNotificationsChange,
|
||||||
|
|
@ -1410,15 +1409,16 @@ private fun PermissionsStep(
|
||||||
}
|
}
|
||||||
PermissionToggleRow(
|
PermissionToggleRow(
|
||||||
title = "Notification listener",
|
title = "Notification listener",
|
||||||
subtitle = "notifications.list and notifications.actions (opens Android Settings)",
|
subtitle = "Read and act on your notifications",
|
||||||
checked = enableNotificationListener,
|
checked = enableNotificationListener,
|
||||||
granted = notificationListenerGranted,
|
granted = notificationListenerGranted,
|
||||||
onCheckedChange = onNotificationListenerChange,
|
onCheckedChange = onNotificationListenerChange,
|
||||||
)
|
)
|
||||||
InlineDivider()
|
|
||||||
|
PermissionSectionHeader("Media")
|
||||||
PermissionToggleRow(
|
PermissionToggleRow(
|
||||||
title = "Microphone",
|
title = "Microphone",
|
||||||
subtitle = "Foreground Voice tab transcription",
|
subtitle = "Voice transcription in the Voice tab",
|
||||||
checked = enableMicrophone,
|
checked = enableMicrophone,
|
||||||
granted = isPermissionGranted(context, Manifest.permission.RECORD_AUDIO),
|
granted = isPermissionGranted(context, Manifest.permission.RECORD_AUDIO),
|
||||||
onCheckedChange = onMicrophoneChange,
|
onCheckedChange = onMicrophoneChange,
|
||||||
|
|
@ -1426,7 +1426,7 @@ private fun PermissionsStep(
|
||||||
InlineDivider()
|
InlineDivider()
|
||||||
PermissionToggleRow(
|
PermissionToggleRow(
|
||||||
title = "Camera",
|
title = "Camera",
|
||||||
subtitle = "camera.snap and camera.clip",
|
subtitle = "Take photos and short video clips",
|
||||||
checked = enableCamera,
|
checked = enableCamera,
|
||||||
granted = isPermissionGranted(context, Manifest.permission.CAMERA),
|
granted = isPermissionGranted(context, Manifest.permission.CAMERA),
|
||||||
onCheckedChange = onCameraChange,
|
onCheckedChange = onCameraChange,
|
||||||
|
|
@ -1434,15 +1434,16 @@ private fun PermissionsStep(
|
||||||
InlineDivider()
|
InlineDivider()
|
||||||
PermissionToggleRow(
|
PermissionToggleRow(
|
||||||
title = "Photos",
|
title = "Photos",
|
||||||
subtitle = "photos.latest",
|
subtitle = "Access your recent photos",
|
||||||
checked = enablePhotos,
|
checked = enablePhotos,
|
||||||
granted = isPermissionGranted(context, photosPermission),
|
granted = isPermissionGranted(context, photosPermission),
|
||||||
onCheckedChange = onPhotosChange,
|
onCheckedChange = onPhotosChange,
|
||||||
)
|
)
|
||||||
InlineDivider()
|
|
||||||
|
PermissionSectionHeader("Personal Data")
|
||||||
PermissionToggleRow(
|
PermissionToggleRow(
|
||||||
title = "Contacts",
|
title = "Contacts",
|
||||||
subtitle = "contacts.search and contacts.add",
|
subtitle = "Search and add contacts",
|
||||||
checked = enableContacts,
|
checked = enableContacts,
|
||||||
granted = contactsGranted,
|
granted = contactsGranted,
|
||||||
onCheckedChange = onContactsChange,
|
onCheckedChange = onContactsChange,
|
||||||
|
|
@ -1450,7 +1451,7 @@ private fun PermissionsStep(
|
||||||
InlineDivider()
|
InlineDivider()
|
||||||
PermissionToggleRow(
|
PermissionToggleRow(
|
||||||
title = "Calendar",
|
title = "Calendar",
|
||||||
subtitle = "calendar.events and calendar.add",
|
subtitle = "Read and create calendar events",
|
||||||
checked = enableCalendar,
|
checked = enableCalendar,
|
||||||
granted = calendarGranted,
|
granted = calendarGranted,
|
||||||
onCheckedChange = onCalendarChange,
|
onCheckedChange = onCalendarChange,
|
||||||
|
|
@ -1458,7 +1459,7 @@ private fun PermissionsStep(
|
||||||
InlineDivider()
|
InlineDivider()
|
||||||
PermissionToggleRow(
|
PermissionToggleRow(
|
||||||
title = "Motion",
|
title = "Motion",
|
||||||
subtitle = "motion.activity and motion.pedometer",
|
subtitle = "Activity and step tracking",
|
||||||
checked = enableMotion,
|
checked = enableMotion,
|
||||||
granted = motionGranted,
|
granted = motionGranted,
|
||||||
onCheckedChange = onMotionChange,
|
onCheckedChange = onMotionChange,
|
||||||
|
|
@ -1469,16 +1470,25 @@ private fun PermissionsStep(
|
||||||
InlineDivider()
|
InlineDivider()
|
||||||
PermissionToggleRow(
|
PermissionToggleRow(
|
||||||
title = "SMS",
|
title = "SMS",
|
||||||
subtitle = "Allow gateway-triggered SMS sending",
|
subtitle = "Send text messages via the gateway",
|
||||||
checked = enableSms,
|
checked = enableSms,
|
||||||
granted = isPermissionGranted(context, Manifest.permission.SEND_SMS),
|
granted = isPermissionGranted(context, Manifest.permission.SEND_SMS),
|
||||||
onCheckedChange = onSmsChange,
|
onCheckedChange = onSmsChange,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text("All settings can be changed later in Settings.", style = onboardingCalloutStyle, color = onboardingTextSecondary)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PermissionSectionHeader(title: String) {
|
||||||
|
Text(
|
||||||
|
title.uppercase(),
|
||||||
|
style = onboardingCaption1Style.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.2.sp),
|
||||||
|
color = onboardingAccent,
|
||||||
|
modifier = Modifier.padding(top = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PermissionToggleRow(
|
private fun PermissionToggleRow(
|
||||||
title: String,
|
title: String,
|
||||||
|
|
@ -1489,6 +1499,12 @@ private fun PermissionToggleRow(
|
||||||
statusOverride: String? = null,
|
statusOverride: String? = null,
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val statusText = statusOverride ?: if (granted) "Granted" else "Not granted"
|
||||||
|
val statusColor = when {
|
||||||
|
statusOverride != null -> onboardingTextTertiary
|
||||||
|
granted -> onboardingSuccess
|
||||||
|
else -> onboardingWarning
|
||||||
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().heightIn(min = 50.dp),
|
modifier = Modifier.fillMaxWidth().heightIn(min = 50.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|
@ -1497,11 +1513,7 @@ private fun PermissionToggleRow(
|
||||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
Text(title, style = onboardingHeadlineStyle, color = onboardingText)
|
Text(title, style = onboardingHeadlineStyle, color = onboardingText)
|
||||||
Text(subtitle, style = onboardingCalloutStyle.copy(lineHeight = 18.sp), color = onboardingTextSecondary)
|
Text(subtitle, style = onboardingCalloutStyle.copy(lineHeight = 18.sp), color = onboardingTextSecondary)
|
||||||
Text(
|
Text(statusText, style = onboardingCaption1Style, color = statusColor)
|
||||||
statusOverride ?: if (granted) "Granted" else "Not granted",
|
|
||||||
style = onboardingCaption1Style,
|
|
||||||
color = if (granted) onboardingSuccess else onboardingTextSecondary,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Switch(
|
Switch(
|
||||||
checked = checked,
|
checked = checked,
|
||||||
|
|
@ -1529,20 +1541,131 @@ private fun FinalStep(
|
||||||
enabledPermissions: String,
|
enabledPermissions: String,
|
||||||
methodLabel: String,
|
methodLabel: String,
|
||||||
) {
|
) {
|
||||||
StepShell(title = "Review") {
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
SummaryField(label = "Method", value = methodLabel)
|
Text("Review", style = onboardingTitle1Style, color = onboardingText)
|
||||||
SummaryField(label = "Gateway", value = parsedGateway?.displayUrl ?: "Invalid gateway URL")
|
|
||||||
SummaryField(label = "Enabled Permissions", value = enabledPermissions)
|
SummaryCard(
|
||||||
|
icon = Icons.Default.Link,
|
||||||
|
label = "Method",
|
||||||
|
value = methodLabel,
|
||||||
|
accentColor = onboardingAccent,
|
||||||
|
)
|
||||||
|
SummaryCard(
|
||||||
|
icon = Icons.Default.Cloud,
|
||||||
|
label = "Gateway",
|
||||||
|
value = parsedGateway?.displayUrl ?: "Invalid gateway URL",
|
||||||
|
accentColor = Color(0xFF7C5AC7),
|
||||||
|
)
|
||||||
|
SummaryCard(
|
||||||
|
icon = Icons.Default.Security,
|
||||||
|
label = "Permissions",
|
||||||
|
value = enabledPermissions,
|
||||||
|
accentColor = onboardingSuccess,
|
||||||
|
)
|
||||||
|
|
||||||
if (!attemptedConnect) {
|
if (!attemptedConnect) {
|
||||||
Text("Press Connect to verify gateway reachability and auth.", style = onboardingCalloutStyle, color = onboardingTextSecondary)
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
color = onboardingAccentSoft,
|
||||||
|
border = androidx.compose.foundation.BorderStroke(1.dp, onboardingAccent.copy(alpha = 0.2f)),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(14.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.size(42.dp)
|
||||||
|
.background(onboardingAccent.copy(alpha = 0.1f), RoundedCornerShape(11.dp)),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Wifi,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = onboardingAccent,
|
||||||
|
modifier = Modifier.size(22.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
"Tap Connect to verify your gateway is reachable.",
|
||||||
|
style = onboardingCalloutStyle,
|
||||||
|
color = onboardingAccent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isConnected) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
color = Color(0xFFEEF9F3),
|
||||||
|
border = androidx.compose.foundation.BorderStroke(1.dp, onboardingSuccess.copy(alpha = 0.2f)),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(14.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.size(42.dp)
|
||||||
|
.background(onboardingSuccess.copy(alpha = 0.1f), RoundedCornerShape(11.dp)),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.CheckCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = onboardingSuccess,
|
||||||
|
modifier = Modifier.size(22.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
|
Text("Connected", style = onboardingHeadlineStyle, color = onboardingSuccess)
|
||||||
|
Text(
|
||||||
|
serverName ?: remoteAddress ?: "gateway",
|
||||||
|
style = onboardingCalloutStyle,
|
||||||
|
color = onboardingSuccess.copy(alpha = 0.8f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Text("Status: $statusText", style = onboardingCalloutStyle, color = if (isConnected) onboardingSuccess else onboardingTextSecondary)
|
Surface(
|
||||||
if (isConnected) {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
Text("Connected to ${serverName ?: remoteAddress ?: "gateway"}", style = onboardingCalloutStyle, color = onboardingSuccess)
|
shape = RoundedCornerShape(14.dp),
|
||||||
} else {
|
color = Color(0xFFFFF8EC),
|
||||||
GuideBlock(title = "Pairing Required") {
|
border = androidx.compose.foundation.BorderStroke(1.dp, onboardingWarning.copy(alpha = 0.2f)),
|
||||||
Text("Run these on the gateway host:", style = onboardingCalloutStyle, color = onboardingTextSecondary)
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(14.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.size(42.dp)
|
||||||
|
.background(onboardingWarning.copy(alpha = 0.1f), RoundedCornerShape(11.dp)),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Link,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = onboardingWarning,
|
||||||
|
modifier = Modifier.size(22.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
|
Text("Pairing Required", style = onboardingHeadlineStyle, color = onboardingWarning)
|
||||||
|
Text("Run these on your gateway host:", style = onboardingCalloutStyle, color = onboardingTextSecondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
CommandBlock("openclaw devices list")
|
CommandBlock("openclaw devices list")
|
||||||
CommandBlock("openclaw devices approve <requestId>")
|
CommandBlock("openclaw devices approve <requestId>")
|
||||||
Text("Then tap Connect again.", style = onboardingCalloutStyle, color = onboardingTextSecondary)
|
Text("Then tap Connect again.", style = onboardingCalloutStyle, color = onboardingTextSecondary)
|
||||||
|
|
@ -1553,15 +1676,46 @@ private fun FinalStep(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SummaryField(label: String, value: String) {
|
private fun SummaryCard(
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
icon: ImageVector,
|
||||||
Text(
|
label: String,
|
||||||
label,
|
value: String,
|
||||||
style = onboardingCaption2Style.copy(fontWeight = FontWeight.SemiBold, letterSpacing = 0.6.sp),
|
accentColor: Color,
|
||||||
color = onboardingTextSecondary,
|
) {
|
||||||
)
|
Surface(
|
||||||
Text(value, style = onboardingHeadlineStyle, color = onboardingText)
|
modifier = Modifier.fillMaxWidth(),
|
||||||
HorizontalDivider(color = onboardingBorder)
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
color = onboardingSurface,
|
||||||
|
border = androidx.compose.foundation.BorderStroke(1.dp, onboardingBorder),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(14.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.size(42.dp)
|
||||||
|
.background(accentColor.copy(alpha = 0.1f), RoundedCornerShape(11.dp)),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = accentColor,
|
||||||
|
modifier = Modifier.size(22.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
|
Text(
|
||||||
|
label.uppercase(),
|
||||||
|
style = onboardingCaption1Style.copy(fontWeight = FontWeight.Bold, letterSpacing = 0.8.sp),
|
||||||
|
color = onboardingTextSecondary,
|
||||||
|
)
|
||||||
|
Text(value, style = onboardingHeadlineStyle, color = onboardingText)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1571,10 +1725,12 @@ private fun CommandBlock(command: String) {
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(onboardingCommandBg, RoundedCornerShape(12.dp))
|
.height(IntrinsicSize.Min)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(onboardingCommandBg)
|
||||||
.border(width = 1.dp, color = onboardingCommandBorder, shape = RoundedCornerShape(12.dp)),
|
.border(width = 1.dp, color = onboardingCommandBorder, shape = RoundedCornerShape(12.dp)),
|
||||||
) {
|
) {
|
||||||
Box(modifier = Modifier.width(3.dp).height(42.dp).background(onboardingCommandAccent))
|
Box(modifier = Modifier.width(3.dp).fillMaxHeight().background(onboardingCommandAccent))
|
||||||
Text(
|
Text(
|
||||||
command,
|
command,
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||||
|
|
@ -1586,23 +1742,42 @@ private fun CommandBlock(command: String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Bullet(text: String) {
|
private fun FeatureCard(
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.Top) {
|
icon: ImageVector,
|
||||||
Box(
|
title: String,
|
||||||
modifier =
|
subtitle: String,
|
||||||
Modifier
|
accentColor: Color,
|
||||||
.padding(top = 7.dp)
|
) {
|
||||||
.size(8.dp)
|
Surface(
|
||||||
.background(onboardingAccentSoft, CircleShape),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
shape = RoundedCornerShape(14.dp),
|
||||||
Box(
|
color = onboardingSurface,
|
||||||
modifier =
|
border = androidx.compose.foundation.BorderStroke(1.dp, onboardingBorder),
|
||||||
Modifier
|
) {
|
||||||
.padding(top = 9.dp)
|
Row(
|
||||||
.size(4.dp)
|
modifier = Modifier.padding(14.dp),
|
||||||
.background(onboardingAccent, CircleShape),
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
)
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
Text(text, style = onboardingBodyStyle, color = onboardingTextSecondary, modifier = Modifier.weight(1f))
|
) {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.size(42.dp)
|
||||||
|
.background(accentColor.copy(alpha = 0.1f), RoundedCornerShape(11.dp)),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = accentColor,
|
||||||
|
modifier = Modifier.size(22.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
|
Text(title, style = onboardingHeadlineStyle, color = onboardingText)
|
||||||
|
Text(subtitle, style = onboardingCalloutStyle, color = onboardingTextSecondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
- tests: align OpenAI Codex auth login expectations with the `gpt-5.4` default model to prevent stale CI failures. (#44367) thanks @jrrcdev
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import * as helpers from "./pi-embedded-helpers.js";
|
|
||||||
import {
|
import {
|
||||||
expectGoogleModelApiFullSanitizeCall,
|
|
||||||
loadSanitizeSessionHistoryWithCleanMocks,
|
loadSanitizeSessionHistoryWithCleanMocks,
|
||||||
makeMockSessionManager,
|
makeMockSessionManager,
|
||||||
makeSimpleUserMessages,
|
makeSimpleUserMessages,
|
||||||
|
type SanitizeSessionHistoryHarness,
|
||||||
sanitizeSnapshotChangedOpenAIReasoning,
|
sanitizeSnapshotChangedOpenAIReasoning,
|
||||||
sanitizeWithOpenAIResponses,
|
sanitizeWithOpenAIResponses,
|
||||||
} from "./pi-embedded-runner.sanitize-session-history.test-harness.js";
|
} from "./pi-embedded-runner.sanitize-session-history.test-harness.js";
|
||||||
|
|
@ -15,42 +14,43 @@ vi.mock("./pi-embedded-helpers.js", async () => ({
|
||||||
sanitizeSessionMessagesImages: vi.fn(async (msgs) => msgs),
|
sanitizeSessionMessagesImages: vi.fn(async (msgs) => msgs),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
type SanitizeSessionHistory = Awaited<ReturnType<typeof loadSanitizeSessionHistoryWithCleanMocks>>;
|
let sanitizeSessionHistory: SanitizeSessionHistoryHarness["sanitizeSessionHistory"];
|
||||||
let sanitizeSessionHistory: SanitizeSessionHistory;
|
let mockedHelpers: SanitizeSessionHistoryHarness["mockedHelpers"];
|
||||||
|
|
||||||
describe("sanitizeSessionHistory e2e smoke", () => {
|
describe("sanitizeSessionHistory e2e smoke", () => {
|
||||||
const mockSessionManager = makeMockSessionManager();
|
const mockSessionManager = makeMockSessionManager();
|
||||||
const mockMessages = makeSimpleUserMessages();
|
const mockMessages = makeSimpleUserMessages();
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
sanitizeSessionHistory = await loadSanitizeSessionHistoryWithCleanMocks();
|
const harness = await loadSanitizeSessionHistoryWithCleanMocks();
|
||||||
|
sanitizeSessionHistory = harness.sanitizeSessionHistory;
|
||||||
|
mockedHelpers = harness.mockedHelpers;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies full sanitize policy for google model APIs", async () => {
|
it("passes simple user-only history through for google model APIs", async () => {
|
||||||
await expectGoogleModelApiFullSanitizeCall({
|
vi.mocked(mockedHelpers.isGoogleModelApi).mockReturnValue(true);
|
||||||
sanitizeSessionHistory,
|
|
||||||
|
const result = await sanitizeSessionHistory({
|
||||||
messages: mockMessages,
|
messages: mockMessages,
|
||||||
|
modelApi: "google-generative-ai",
|
||||||
|
provider: "google-vertex",
|
||||||
sessionManager: mockSessionManager,
|
sessionManager: mockSessionManager,
|
||||||
|
sessionId: "test-session",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(mockMessages);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps images-only sanitize policy without tool-call id rewriting for openai-responses", async () => {
|
it("passes simple user-only history through for openai-responses", async () => {
|
||||||
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
vi.mocked(mockedHelpers.isGoogleModelApi).mockReturnValue(false);
|
||||||
|
|
||||||
await sanitizeWithOpenAIResponses({
|
const result = await sanitizeWithOpenAIResponses({
|
||||||
sanitizeSessionHistory,
|
sanitizeSessionHistory,
|
||||||
messages: mockMessages,
|
messages: mockMessages,
|
||||||
sessionManager: mockSessionManager,
|
sessionManager: mockSessionManager,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(helpers.sanitizeSessionMessagesImages).toHaveBeenCalledWith(
|
expect(result).toEqual(mockMessages);
|
||||||
mockMessages,
|
|
||||||
"session:history",
|
|
||||||
expect.objectContaining({
|
|
||||||
sanitizeMode: "images-only",
|
|
||||||
sanitizeToolCallIds: false,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("downgrades openai reasoning blocks when the model snapshot changed", async () => {
|
it("downgrades openai reasoning blocks when the model snapshot changed", async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
import type { SessionManager } from "@mariozechner/pi-coding-agent";
|
import type { SessionManager } from "@mariozechner/pi-coding-agent";
|
||||||
import { expect, vi } from "vitest";
|
import { expect, vi } from "vitest";
|
||||||
import * as helpers from "./pi-embedded-helpers.js";
|
|
||||||
|
|
||||||
export type SessionEntry = { type: string; customType: string; data: unknown };
|
export type SessionEntry = { type: string; customType: string; data: unknown };
|
||||||
export type SanitizeSessionHistoryFn = (params: {
|
export type SanitizeSessionHistoryFn = (params: {
|
||||||
|
|
@ -13,6 +12,11 @@ export type SanitizeSessionHistoryFn = (params: {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
modelId?: string;
|
modelId?: string;
|
||||||
}) => Promise<AgentMessage[]>;
|
}) => Promise<AgentMessage[]>;
|
||||||
|
export type SanitizeSessionHistoryMockedHelpers = typeof import("./pi-embedded-helpers.js");
|
||||||
|
export type SanitizeSessionHistoryHarness = {
|
||||||
|
sanitizeSessionHistory: SanitizeSessionHistoryFn;
|
||||||
|
mockedHelpers: SanitizeSessionHistoryMockedHelpers;
|
||||||
|
};
|
||||||
export const TEST_SESSION_ID = "test-session";
|
export const TEST_SESSION_ID = "test-session";
|
||||||
|
|
||||||
export function makeModelSnapshotEntry(data: {
|
export function makeModelSnapshotEntry(data: {
|
||||||
|
|
@ -54,11 +58,16 @@ export function makeSimpleUserMessages(): AgentMessage[] {
|
||||||
return messages as unknown as AgentMessage[];
|
return messages as unknown as AgentMessage[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadSanitizeSessionHistoryWithCleanMocks(): Promise<SanitizeSessionHistoryFn> {
|
export async function loadSanitizeSessionHistoryWithCleanMocks(): Promise<SanitizeSessionHistoryHarness> {
|
||||||
|
vi.resetModules();
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
vi.mocked(helpers.sanitizeSessionMessagesImages).mockImplementation(async (msgs) => msgs);
|
const mockedHelpers = await import("./pi-embedded-helpers.js");
|
||||||
|
vi.mocked(mockedHelpers.sanitizeSessionMessagesImages).mockImplementation(async (msgs) => msgs);
|
||||||
const mod = await import("./pi-embedded-runner/google.js");
|
const mod = await import("./pi-embedded-runner/google.js");
|
||||||
return mod.sanitizeSessionHistory;
|
return {
|
||||||
|
sanitizeSessionHistory: mod.sanitizeSessionHistory,
|
||||||
|
mockedHelpers,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeReasoningAssistantMessages(opts?: {
|
export function makeReasoningAssistantMessages(opts?: {
|
||||||
|
|
@ -118,26 +127,6 @@ export function expectOpenAIResponsesStrictSanitizeCall(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function expectGoogleModelApiFullSanitizeCall(params: {
|
|
||||||
sanitizeSessionHistory: SanitizeSessionHistoryFn;
|
|
||||||
messages: AgentMessage[];
|
|
||||||
sessionManager: SessionManager;
|
|
||||||
}) {
|
|
||||||
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(true);
|
|
||||||
await params.sanitizeSessionHistory({
|
|
||||||
messages: params.messages,
|
|
||||||
modelApi: "google-generative-ai",
|
|
||||||
provider: "google-vertex",
|
|
||||||
sessionManager: params.sessionManager,
|
|
||||||
sessionId: TEST_SESSION_ID,
|
|
||||||
});
|
|
||||||
expect(helpers.sanitizeSessionMessagesImages).toHaveBeenCalledWith(
|
|
||||||
params.messages,
|
|
||||||
"session:history",
|
|
||||||
expect.objectContaining({ sanitizeMode: "full", sanitizeToolCallIds: true }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function makeSnapshotChangedOpenAIReasoningScenario() {
|
export function makeSnapshotChangedOpenAIReasoningScenario() {
|
||||||
const sessionEntries = [
|
const sessionEntries = [
|
||||||
makeModelSnapshotEntry({
|
makeModelSnapshotEntry({
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
import type { AssistantMessage, UserMessage, Usage } from "@mariozechner/pi-ai";
|
import type { AssistantMessage, UserMessage, Usage } from "@mariozechner/pi-ai";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import * as helpers from "./pi-embedded-helpers.js";
|
|
||||||
import {
|
import {
|
||||||
expectGoogleModelApiFullSanitizeCall,
|
|
||||||
loadSanitizeSessionHistoryWithCleanMocks,
|
loadSanitizeSessionHistoryWithCleanMocks,
|
||||||
makeMockSessionManager,
|
makeMockSessionManager,
|
||||||
makeInMemorySessionManager,
|
makeInMemorySessionManager,
|
||||||
|
|
@ -11,6 +9,7 @@ import {
|
||||||
makeReasoningAssistantMessages,
|
makeReasoningAssistantMessages,
|
||||||
makeSimpleUserMessages,
|
makeSimpleUserMessages,
|
||||||
sanitizeSnapshotChangedOpenAIReasoning,
|
sanitizeSnapshotChangedOpenAIReasoning,
|
||||||
|
type SanitizeSessionHistoryHarness,
|
||||||
type SanitizeSessionHistoryFn,
|
type SanitizeSessionHistoryFn,
|
||||||
sanitizeWithOpenAIResponses,
|
sanitizeWithOpenAIResponses,
|
||||||
TEST_SESSION_ID,
|
TEST_SESSION_ID,
|
||||||
|
|
@ -25,6 +24,7 @@ vi.mock("./pi-embedded-helpers.js", async () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let sanitizeSessionHistory: SanitizeSessionHistoryFn;
|
let sanitizeSessionHistory: SanitizeSessionHistoryFn;
|
||||||
|
let mockedHelpers: SanitizeSessionHistoryHarness["mockedHelpers"];
|
||||||
let testTimestamp = 1;
|
let testTimestamp = 1;
|
||||||
const nextTimestamp = () => testTimestamp++;
|
const nextTimestamp = () => testTimestamp++;
|
||||||
|
|
||||||
|
|
@ -35,7 +35,7 @@ describe("sanitizeSessionHistory", () => {
|
||||||
const mockSessionManager = makeMockSessionManager();
|
const mockSessionManager = makeMockSessionManager();
|
||||||
const mockMessages = makeSimpleUserMessages();
|
const mockMessages = makeSimpleUserMessages();
|
||||||
const setNonGoogleModelApi = () => {
|
const setNonGoogleModelApi = () => {
|
||||||
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
vi.mocked(mockedHelpers.isGoogleModelApi).mockReturnValue(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizeGithubCopilotHistory = async (params: {
|
const sanitizeGithubCopilotHistory = async (params: {
|
||||||
|
|
@ -164,21 +164,29 @@ describe("sanitizeSessionHistory", () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
testTimestamp = 1;
|
testTimestamp = 1;
|
||||||
sanitizeSessionHistory = await loadSanitizeSessionHistoryWithCleanMocks();
|
const harness = await loadSanitizeSessionHistoryWithCleanMocks();
|
||||||
|
sanitizeSessionHistory = harness.sanitizeSessionHistory;
|
||||||
|
mockedHelpers = harness.mockedHelpers;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sanitizes tool call ids for Google model APIs", async () => {
|
it("passes simple user-only history through for Google model APIs", async () => {
|
||||||
await expectGoogleModelApiFullSanitizeCall({
|
vi.mocked(mockedHelpers.isGoogleModelApi).mockReturnValue(true);
|
||||||
sanitizeSessionHistory,
|
|
||||||
|
const result = await sanitizeSessionHistory({
|
||||||
messages: mockMessages,
|
messages: mockMessages,
|
||||||
|
modelApi: "google-generative-ai",
|
||||||
|
provider: "google-vertex",
|
||||||
sessionManager: mockSessionManager,
|
sessionManager: mockSessionManager,
|
||||||
|
sessionId: TEST_SESSION_ID,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(mockMessages);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sanitizes tool call ids with strict9 for Mistral models", async () => {
|
it("passes simple user-only history through for Mistral models", async () => {
|
||||||
setNonGoogleModelApi();
|
setNonGoogleModelApi();
|
||||||
|
|
||||||
await sanitizeSessionHistory({
|
const result = await sanitizeSessionHistory({
|
||||||
messages: mockMessages,
|
messages: mockMessages,
|
||||||
modelApi: "openai-responses",
|
modelApi: "openai-responses",
|
||||||
provider: "openrouter",
|
provider: "openrouter",
|
||||||
|
|
@ -187,21 +195,13 @@ describe("sanitizeSessionHistory", () => {
|
||||||
sessionId: TEST_SESSION_ID,
|
sessionId: TEST_SESSION_ID,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(helpers.sanitizeSessionMessagesImages).toHaveBeenCalledWith(
|
expect(result).toEqual(mockMessages);
|
||||||
mockMessages,
|
|
||||||
"session:history",
|
|
||||||
expect.objectContaining({
|
|
||||||
sanitizeMode: "full",
|
|
||||||
sanitizeToolCallIds: true,
|
|
||||||
toolCallIdMode: "strict9",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sanitizes tool call ids for Anthropic APIs", async () => {
|
it("passes simple user-only history through for Anthropic APIs", async () => {
|
||||||
setNonGoogleModelApi();
|
setNonGoogleModelApi();
|
||||||
|
|
||||||
await sanitizeSessionHistory({
|
const result = await sanitizeSessionHistory({
|
||||||
messages: mockMessages,
|
messages: mockMessages,
|
||||||
modelApi: "anthropic-messages",
|
modelApi: "anthropic-messages",
|
||||||
provider: "anthropic",
|
provider: "anthropic",
|
||||||
|
|
@ -209,33 +209,25 @@ describe("sanitizeSessionHistory", () => {
|
||||||
sessionId: TEST_SESSION_ID,
|
sessionId: TEST_SESSION_ID,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(helpers.sanitizeSessionMessagesImages).toHaveBeenCalledWith(
|
expect(result).toEqual(mockMessages);
|
||||||
mockMessages,
|
|
||||||
"session:history",
|
|
||||||
expect.objectContaining({ sanitizeMode: "full", sanitizeToolCallIds: true }),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not sanitize tool call ids for openai-responses", async () => {
|
it("passes simple user-only history through for openai-responses", async () => {
|
||||||
setNonGoogleModelApi();
|
setNonGoogleModelApi();
|
||||||
|
|
||||||
await sanitizeWithOpenAIResponses({
|
const result = await sanitizeWithOpenAIResponses({
|
||||||
sanitizeSessionHistory,
|
sanitizeSessionHistory,
|
||||||
messages: mockMessages,
|
messages: mockMessages,
|
||||||
sessionManager: mockSessionManager,
|
sessionManager: mockSessionManager,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(helpers.sanitizeSessionMessagesImages).toHaveBeenCalledWith(
|
expect(result).toEqual(mockMessages);
|
||||||
mockMessages,
|
|
||||||
"session:history",
|
|
||||||
expect.objectContaining({ sanitizeMode: "images-only", sanitizeToolCallIds: false }),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sanitizes tool call ids for openai-completions", async () => {
|
it("passes simple user-only history through for openai-completions", async () => {
|
||||||
setNonGoogleModelApi();
|
setNonGoogleModelApi();
|
||||||
|
|
||||||
await sanitizeSessionHistory({
|
const result = await sanitizeSessionHistory({
|
||||||
messages: mockMessages,
|
messages: mockMessages,
|
||||||
modelApi: "openai-completions",
|
modelApi: "openai-completions",
|
||||||
provider: "openai",
|
provider: "openai",
|
||||||
|
|
@ -244,15 +236,7 @@ describe("sanitizeSessionHistory", () => {
|
||||||
sessionId: TEST_SESSION_ID,
|
sessionId: TEST_SESSION_ID,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(helpers.sanitizeSessionMessagesImages).toHaveBeenCalledWith(
|
expect(result).toEqual(mockMessages);
|
||||||
mockMessages,
|
|
||||||
"session:history",
|
|
||||||
expect.objectContaining({
|
|
||||||
sanitizeMode: "images-only",
|
|
||||||
sanitizeToolCallIds: true,
|
|
||||||
toolCallIdMode: "strict",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prepends a bootstrap user turn for strict OpenAI-compatible assistant-first history", async () => {
|
it("prepends a bootstrap user turn for strict OpenAI-compatible assistant-first history", async () => {
|
||||||
|
|
@ -314,7 +298,7 @@ describe("sanitizeSessionHistory", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("drops stale assistant usage snapshots kept before latest compaction summary", async () => {
|
it("drops stale assistant usage snapshots kept before latest compaction summary", async () => {
|
||||||
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
vi.mocked(mockedHelpers.isGoogleModelApi).mockReturnValue(false);
|
||||||
|
|
||||||
const messages = castAgentMessages([
|
const messages = castAgentMessages([
|
||||||
{ role: "user", content: "old context" },
|
{ role: "user", content: "old context" },
|
||||||
|
|
@ -335,7 +319,7 @@ describe("sanitizeSessionHistory", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves fresh assistant usage snapshots created after latest compaction summary", async () => {
|
it("preserves fresh assistant usage snapshots created after latest compaction summary", async () => {
|
||||||
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
vi.mocked(mockedHelpers.isGoogleModelApi).mockReturnValue(false);
|
||||||
|
|
||||||
const messages = castAgentMessages([
|
const messages = castAgentMessages([
|
||||||
makeAssistantUsageMessage({
|
makeAssistantUsageMessage({
|
||||||
|
|
@ -359,7 +343,7 @@ describe("sanitizeSessionHistory", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds a zeroed assistant usage snapshot when usage is missing", async () => {
|
it("adds a zeroed assistant usage snapshot when usage is missing", async () => {
|
||||||
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
vi.mocked(mockedHelpers.isGoogleModelApi).mockReturnValue(false);
|
||||||
|
|
||||||
const messages = castAgentMessages([
|
const messages = castAgentMessages([
|
||||||
{ role: "user", content: "question" },
|
{ role: "user", content: "question" },
|
||||||
|
|
@ -378,7 +362,7 @@ describe("sanitizeSessionHistory", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("normalizes mixed partial assistant usage fields to numeric totals", async () => {
|
it("normalizes mixed partial assistant usage fields to numeric totals", async () => {
|
||||||
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
vi.mocked(mockedHelpers.isGoogleModelApi).mockReturnValue(false);
|
||||||
|
|
||||||
const messages = castAgentMessages([
|
const messages = castAgentMessages([
|
||||||
{ role: "user", content: "question" },
|
{ role: "user", content: "question" },
|
||||||
|
|
@ -407,7 +391,7 @@ describe("sanitizeSessionHistory", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves existing usage cost while normalizing token fields", async () => {
|
it("preserves existing usage cost while normalizing token fields", async () => {
|
||||||
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
vi.mocked(mockedHelpers.isGoogleModelApi).mockReturnValue(false);
|
||||||
|
|
||||||
const messages = castAgentMessages([
|
const messages = castAgentMessages([
|
||||||
{ role: "user", content: "question" },
|
{ role: "user", content: "question" },
|
||||||
|
|
@ -451,7 +435,7 @@ describe("sanitizeSessionHistory", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves unknown cost when token fields already match", async () => {
|
it("preserves unknown cost when token fields already match", async () => {
|
||||||
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
vi.mocked(mockedHelpers.isGoogleModelApi).mockReturnValue(false);
|
||||||
|
|
||||||
const messages = castAgentMessages([
|
const messages = castAgentMessages([
|
||||||
{ role: "user", content: "question" },
|
{ role: "user", content: "question" },
|
||||||
|
|
@ -484,7 +468,7 @@ describe("sanitizeSessionHistory", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("drops stale usage when compaction summary appears before kept assistant messages", async () => {
|
it("drops stale usage when compaction summary appears before kept assistant messages", async () => {
|
||||||
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
vi.mocked(mockedHelpers.isGoogleModelApi).mockReturnValue(false);
|
||||||
|
|
||||||
const compactionTs = Date.parse("2026-02-26T12:00:00.000Z");
|
const compactionTs = Date.parse("2026-02-26T12:00:00.000Z");
|
||||||
const messages = castAgentMessages([
|
const messages = castAgentMessages([
|
||||||
|
|
@ -505,7 +489,7 @@ describe("sanitizeSessionHistory", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps fresh usage after compaction timestamp in summary-first ordering", async () => {
|
it("keeps fresh usage after compaction timestamp in summary-first ordering", async () => {
|
||||||
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
vi.mocked(mockedHelpers.isGoogleModelApi).mockReturnValue(false);
|
||||||
|
|
||||||
const compactionTs = Date.parse("2026-02-26T12:00:00.000Z");
|
const compactionTs = Date.parse("2026-02-26T12:00:00.000Z");
|
||||||
const messages = castAgentMessages([
|
const messages = castAgentMessages([
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ describe("modelsAuthLoginCommand", () => {
|
||||||
"Auth profile: openai-codex:user@example.com (openai-codex/oauth)",
|
"Auth profile: openai-codex:user@example.com (openai-codex/oauth)",
|
||||||
);
|
);
|
||||||
expect(runtime.log).toHaveBeenCalledWith(
|
expect(runtime.log).toHaveBeenCalledWith(
|
||||||
"Default model available: openai-codex/gpt-5.3-codex (use --set-default to apply)",
|
"Default model available: openai-codex/gpt-5.4 (use --set-default to apply)",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -193,9 +193,9 @@ describe("modelsAuthLoginCommand", () => {
|
||||||
await modelsAuthLoginCommand({ provider: "openai-codex", setDefault: true }, runtime);
|
await modelsAuthLoginCommand({ provider: "openai-codex", setDefault: true }, runtime);
|
||||||
|
|
||||||
expect(lastUpdatedConfig?.agents?.defaults?.model).toEqual({
|
expect(lastUpdatedConfig?.agents?.defaults?.model).toEqual({
|
||||||
primary: "openai-codex/gpt-5.3-codex",
|
primary: "openai-codex/gpt-5.4",
|
||||||
});
|
});
|
||||||
expect(runtime.log).toHaveBeenCalledWith("Default model set to openai-codex/gpt-5.3-codex");
|
expect(runtime.log).toHaveBeenCalledWith("Default model set to openai-codex/gpt-5.4");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps existing plugin error behavior for non built-in providers", async () => {
|
it("keeps existing plugin error behavior for non built-in providers", async () => {
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,7 @@ describe("gateway server sessions", () => {
|
||||||
lastChannel: "whatsapp",
|
lastChannel: "whatsapp",
|
||||||
lastTo: "+1555",
|
lastTo: "+1555",
|
||||||
lastAccountId: "work",
|
lastAccountId: "work",
|
||||||
|
lastThreadId: "1737500000.123456",
|
||||||
},
|
},
|
||||||
"discord:group:dev": {
|
"discord:group:dev": {
|
||||||
sessionId: "sess-group",
|
sessionId: "sess-group",
|
||||||
|
|
@ -336,6 +337,7 @@ describe("gateway server sessions", () => {
|
||||||
channel: "whatsapp",
|
channel: "whatsapp",
|
||||||
to: "+1555",
|
to: "+1555",
|
||||||
accountId: "work",
|
accountId: "work",
|
||||||
|
threadId: "1737500000.123456",
|
||||||
});
|
});
|
||||||
|
|
||||||
const active = await rpcReq<{
|
const active = await rpcReq<{
|
||||||
|
|
@ -545,13 +547,27 @@ describe("gateway server sessions", () => {
|
||||||
const reset = await rpcReq<{
|
const reset = await rpcReq<{
|
||||||
ok: true;
|
ok: true;
|
||||||
key: string;
|
key: string;
|
||||||
entry: { sessionId: string; modelProvider?: string; model?: string };
|
entry: {
|
||||||
|
sessionId: string;
|
||||||
|
modelProvider?: string;
|
||||||
|
model?: string;
|
||||||
|
lastAccountId?: string;
|
||||||
|
lastThreadId?: string | number;
|
||||||
|
};
|
||||||
}>(ws, "sessions.reset", { key: "agent:main:main" });
|
}>(ws, "sessions.reset", { key: "agent:main:main" });
|
||||||
expect(reset.ok).toBe(true);
|
expect(reset.ok).toBe(true);
|
||||||
expect(reset.payload?.key).toBe("agent:main:main");
|
expect(reset.payload?.key).toBe("agent:main:main");
|
||||||
expect(reset.payload?.entry.sessionId).not.toBe("sess-main");
|
expect(reset.payload?.entry.sessionId).not.toBe("sess-main");
|
||||||
expect(reset.payload?.entry.modelProvider).toBe("openai");
|
expect(reset.payload?.entry.modelProvider).toBe("openai");
|
||||||
expect(reset.payload?.entry.model).toBe("gpt-test-a");
|
expect(reset.payload?.entry.model).toBe("gpt-test-a");
|
||||||
|
expect(reset.payload?.entry.lastAccountId).toBe("work");
|
||||||
|
expect(reset.payload?.entry.lastThreadId).toBe("1737500000.123456");
|
||||||
|
const storeAfterReset = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
|
||||||
|
string,
|
||||||
|
{ lastAccountId?: string; lastThreadId?: string | number }
|
||||||
|
>;
|
||||||
|
expect(storeAfterReset["agent:main:main"]?.lastAccountId).toBe("work");
|
||||||
|
expect(storeAfterReset["agent:main:main"]?.lastThreadId).toBe("1737500000.123456");
|
||||||
const filesAfterReset = await fs.readdir(dir);
|
const filesAfterReset = await fs.readdir(dir);
|
||||||
expect(filesAfterReset.some((f) => f.startsWith("sess-main.jsonl.reset."))).toBe(true);
|
expect(filesAfterReset.some((f) => f.startsWith("sess-main.jsonl.reset."))).toBe(true);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -338,6 +338,8 @@ export async function performGatewaySessionReset(params: {
|
||||||
origin: snapshotSessionOrigin(currentEntry),
|
origin: snapshotSessionOrigin(currentEntry),
|
||||||
lastChannel: currentEntry?.lastChannel,
|
lastChannel: currentEntry?.lastChannel,
|
||||||
lastTo: currentEntry?.lastTo,
|
lastTo: currentEntry?.lastTo,
|
||||||
|
lastAccountId: currentEntry?.lastAccountId,
|
||||||
|
lastThreadId: currentEntry?.lastThreadId,
|
||||||
skillsSnapshot: currentEntry?.skillsSnapshot,
|
skillsSnapshot: currentEntry?.skillsSnapshot,
|
||||||
inputTokens: 0,
|
inputTokens: 0,
|
||||||
outputTokens: 0,
|
outputTokens: 0,
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,22 @@ function maybeBootstrapChannelPlugin(params: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveDirectFromActiveRegistry(
|
||||||
|
channel: DeliverableMessageChannel,
|
||||||
|
): ChannelPlugin | undefined {
|
||||||
|
const activeRegistry = getActivePluginRegistry();
|
||||||
|
if (!activeRegistry) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
for (const entry of activeRegistry.channels) {
|
||||||
|
const plugin = entry?.plugin;
|
||||||
|
if (plugin?.id === channel) {
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveOutboundChannelPlugin(params: {
|
export function resolveOutboundChannelPlugin(params: {
|
||||||
channel: string;
|
channel: string;
|
||||||
cfg?: OpenClawConfig;
|
cfg?: OpenClawConfig;
|
||||||
|
|
@ -72,7 +88,11 @@ export function resolveOutboundChannelPlugin(params: {
|
||||||
if (current) {
|
if (current) {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
const directCurrent = resolveDirectFromActiveRegistry(normalized);
|
||||||
|
if (directCurrent) {
|
||||||
|
return directCurrent;
|
||||||
|
}
|
||||||
|
|
||||||
maybeBootstrapChannelPlugin({ channel: normalized, cfg: params.cfg });
|
maybeBootstrapChannelPlugin({ channel: normalized, cfg: params.cfg });
|
||||||
return resolve();
|
return resolve() ?? resolveDirectFromActiveRegistry(normalized);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { telegramOutbound } from "../../channels/plugins/outbound/telegram.js";
|
||||||
import type { OpenClawConfig } from "../../config/config.js";
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
|
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||||
|
import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||||
import {
|
import {
|
||||||
resolveHeartbeatDeliveryTarget,
|
resolveHeartbeatDeliveryTarget,
|
||||||
resolveOutboundTarget,
|
resolveOutboundTarget,
|
||||||
|
|
@ -64,6 +67,27 @@ describe("resolveOutboundTarget defaultTo config fallback", () => {
|
||||||
});
|
});
|
||||||
expect(res.ok).toBe(false);
|
expect(res.ok).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("falls back to the active registry when the cached channel map is stale", () => {
|
||||||
|
const registry = createTestRegistry([]);
|
||||||
|
setActivePluginRegistry(registry, "stale-registry-test");
|
||||||
|
|
||||||
|
// Warm the cached channel map before mutating the registry in place.
|
||||||
|
expect(resolveOutboundTarget({ channel: "telegram", to: "123", mode: "explicit" }).ok).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
registry.channels.push({
|
||||||
|
pluginId: "telegram",
|
||||||
|
plugin: createOutboundTestPlugin({ id: "telegram", outbound: telegramOutbound }),
|
||||||
|
source: "test",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolveOutboundTarget({ channel: "telegram", to: "123", mode: "explicit" })).toEqual({
|
||||||
|
ok: true,
|
||||||
|
to: "123",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("resolveSessionDeliveryTarget", () => {
|
describe("resolveSessionDeliveryTarget", () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue