Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import androidx.navigation3.runtime.NavKey
import org.wip.plugintoolkit.features.navigation.GlobalRouter
import org.wip.plugintoolkit.features.navigation.model.Screen
import org.wip.plugintoolkit.features.plugin.logic.PluginManager
import org.wip.plugintoolkit.features.plugin.model.resolveProvidedValues
import org.wip.plugintoolkit.features.plugin.ui.lockedClickInterceptor
import org.wip.plugintoolkit.shared.components.ToolkitTextField
import plugintoolkit.composeapp.generated.resources.Res
Expand Down Expand Up @@ -254,8 +255,9 @@ private fun CapabilitiesPalette(
)
)
caps.forEach { cap ->
val isReady = remember(cap, settingsStore.settings, manifest?.settings) {
cap.isReady(settingsStore.settings, manifest?.settings)
val providedSettings = settingsStore.resolveProvidedValues(manifest)
val isReady = remember(cap, providedSettings, manifest?.settings) {
cap.isReady(providedSettings, manifest?.settings)
}

val targetSettingKey = cap.requiredLocks.firstOrNull()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import org.wip.plugintoolkit.core.utils.FileSystem
import org.wip.plugintoolkit.features.job.logic.JobManager
import org.wip.plugintoolkit.features.job.model.JobStatus
import org.wip.plugintoolkit.features.plugin.model.PluginSettingsStore
import org.wip.plugintoolkit.features.plugin.model.resolveCustomSettings
import org.wip.plugintoolkit.features.settings.logic.SettingsRepository
import org.wip.plugintoolkit.features.settings.model.PluginUnplugBehavior
import org.wip.plugintoolkit.features.plugin.utils.PluginCompatibilityUtils
Expand Down Expand Up @@ -332,15 +333,7 @@ class PluginLifecycleManager(

val storedSettings = overriddenSettings ?: loadPluginSettings(pkg)
val actualManifest = manifest ?: getManifest(pkg)
val mergedSettings = mutableMapOf<String, JsonElement>()

// 1. Manifest defaults
actualManifest?.settings?.forEach { (key, meta) ->
meta.defaultValue?.let { mergedSettings[key] = it }
}

// 2. User overrides
mergedSettings.putAll(storedSettings.settings)
val mergedSettings = storedSettings.resolveCustomSettings(actualManifest)

val pluginLogger = jobManager.getPluginLogger(pkg, jobId)
val progressReporter = object : ProgressReporter {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,23 @@ package org.wip.plugintoolkit.features.plugin.model

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import org.wip.plugintoolkit.api.PluginManifest

@Serializable
data class PluginSettingsStore(
val settings: Map<String, JsonElement> = emptyMap(),
val globalParams: Map<String, JsonElement> = emptyMap(),
val capabilityParams: Map<String, Map<String, JsonElement>> = emptyMap()
)

fun PluginManifest.defaultCustomSettings(): Map<String, JsonElement> = settings.orEmpty().mapNotNull { (key, metadata) ->
metadata.defaultValue?.let { key to it }
}.toMap()

/** Manifest defaults with persisted user values taking precedence. */
fun PluginSettingsStore.resolveCustomSettings(manifest: PluginManifest?): Map<String, JsonElement> =
(manifest?.defaultCustomSettings() ?: emptyMap()) + settings

/** Values available to generated inputs and lock evaluation in the settings UI. */
fun PluginSettingsStore.resolveProvidedValues(manifest: PluginManifest?): Map<String, JsonElement> =
resolveCustomSettings(manifest) + globalParams
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import org.wip.plugintoolkit.api.Capability
import org.wip.plugintoolkit.api.PluginEntry
import org.wip.plugintoolkit.core.model.localized
import org.wip.plugintoolkit.core.theme.ToolkitTheme
import org.wip.plugintoolkit.features.plugin.model.resolveProvidedValues
import org.wip.plugintoolkit.features.navigation.GlobalRouter
import org.wip.plugintoolkit.features.navigation.LocalGlobalRouter
import org.wip.plugintoolkit.features.navigation.model.Screen
Expand Down Expand Up @@ -172,7 +173,7 @@ fun DirectExecutionSidebar(
val manifest = plugin.getManifest().getOrThrow()
val pluginManager: org.wip.plugintoolkit.features.plugin.logic.PluginManager = org.koin.compose.koinInject()
val settingsStore = pluginManager.loadPluginSettings(pluginId)
val settings = settingsStore.settings + settingsStore.globalParams
val settings = settingsStore.resolveProvidedValues(manifest)
val pluginLocksState by pluginManager.pluginLocksState.collectAsState()
val locks = pluginLocksState[pluginId] ?: pluginLocksState.values.fold(emptyMap<String, Boolean>()) { acc, map -> acc + map }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import kotlinx.serialization.json.JsonPrimitive
import org.wip.plugintoolkit.api.DataType
import org.wip.plugintoolkit.api.PrimitiveType
import org.jetbrains.compose.resources.stringResource
import org.wip.plugintoolkit.api.Capability
import org.wip.plugintoolkit.api.ParameterRole
import org.wip.plugintoolkit.api.PluginManifest
import org.wip.plugintoolkit.features.job.model.BackgroundJob
import org.wip.plugintoolkit.features.job.model.JobStatus
import org.wip.plugintoolkit.features.plugin.model.resolveProvidedValues
import org.wip.plugintoolkit.features.navigation.model.Screen
import org.wip.plugintoolkit.features.plugin.viewmodel.PluginViewModel
import org.wip.plugintoolkit.shared.components.plugin.JobResultCard
Expand Down Expand Up @@ -122,12 +120,8 @@ fun PluginContent(
val providedSettings = remember(pluginId, pluginSettingsState) {
val store = if (pluginId != null) pluginSettingsState[pluginId] ?: pluginManager.loadPluginSettings(pluginId) else null
val manifest = viewModel.selectedPlugin?.getManifest()?.getOrNull()
val manifestDefaults = (manifest?.settings?.mapValues { (_, meta) ->
meta.defaultValue ?: if (meta.type is DataType.Primitive && (meta.type as DataType.Primitive).primitiveType == PrimitiveType.BOOLEAN) {
JsonPrimitive(false)
} else null
}?.filterValues { it != null } ?: emptyMap()) as Map<String, kotlinx.serialization.json.JsonElement>
manifestDefaults + (store?.settings ?: emptyMap()) + (store?.globalParams ?: emptyMap())
(store ?: org.wip.plugintoolkit.features.plugin.model.PluginSettingsStore())
.resolveProvidedValues(manifest)
}

if (selectedCapability == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import org.wip.plugintoolkit.api.PluginAction
import org.wip.plugintoolkit.api.PrimitiveType
import org.wip.plugintoolkit.core.model.localized
import org.wip.plugintoolkit.core.theme.ToolkitTheme
import org.wip.plugintoolkit.features.plugin.model.resolveCustomSettings
import org.wip.plugintoolkit.features.plugin.utils.SettingsUtils
import org.wip.plugintoolkit.features.plugin.viewmodel.PluginSettingsViewModel
import org.wip.plugintoolkit.shared.components.ToolkitChip
Expand Down Expand Up @@ -410,15 +411,8 @@ fun PluginSettingsContent(
)
}
} else {
val manifestDefaults = remember(manifest) {
(manifest.settings?.mapValues { (_, meta) ->
meta.defaultValue ?: if (meta.type is DataType.Primitive && (meta.type as DataType.Primitive).primitiveType == PrimitiveType.BOOLEAN) {
JsonPrimitive(false)
} else null
}?.filterValues { it != null } ?: emptyMap()) as Map<String, kotlinx.serialization.json.JsonElement>
}
val providedSettings = remember(manifestDefaults, store.settings) {
manifestDefaults + store.settings
val providedSettings = remember(manifest, store.settings) {
store.resolveCustomSettings(manifest)
}

LazyColumn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ import org.wip.plugintoolkit.core.utils.FileSystem
import org.wip.plugintoolkit.features.job.logic.JobManager
import org.wip.plugintoolkit.features.plugin.model.InstalledPlugin
import org.wip.plugintoolkit.features.plugin.model.PluginSettingsStore
import org.wip.plugintoolkit.features.plugin.model.resolveCustomSettings
import org.wip.plugintoolkit.features.settings.logic.SettingsPersistence
import org.wip.plugintoolkit.features.settings.logic.SettingsRepository
import org.wip.plugintoolkit.features.settings.model.AppSettings
import org.wip.plugintoolkit.api.DataType
import org.wip.plugintoolkit.api.PluginInfo
import org.wip.plugintoolkit.api.PluginManifest
import org.wip.plugintoolkit.api.PrimitiveType
import org.wip.plugintoolkit.api.Requirements
import org.wip.plugintoolkit.api.SettingMetadata
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotSame
Expand Down Expand Up @@ -48,6 +55,58 @@ class PluginLifecycleManagerTest {
override fun openLatestLog() {}
}

@Test
fun testPluginContextReceivesExactlyResolvedCustomSettings() = runTest {
val fileSystem = FakeFileSystem()
val settingsRepo = SettingsRepository(FakeSettingsPersistence(), backgroundScope)
val registry = PluginRegistry(
settingsRepo,
backgroundScope,
loomDispatcher,
io.mockk.mockk(relaxed = true)
)
val lifecycleManager = PluginLifecycleManager(
registry,
JobManager(backgroundScope, settingsRepo),
settingsRepo,
fileSystem
)
val pkg = "test.context.defaults"
registry.addOrUpdatePlugin(
InstalledPlugin(pkg, "Test", "1.0.0", "/tmp/test.context.defaults")
)
val manifest = PluginManifest(
manifestVersion = "1",
plugin = PluginInfo(pkg, "Test", "1.0.0", "Test plugin"),
requirements = Requirements(128, 10),
settings = mapOf(
"endpoint" to SettingMetadata(
defaultValue = JsonPrimitive("https://default.test"),
description = "Endpoint",
type = DataType.Primitive(PrimitiveType.STRING)
),
"optionalFlag" to SettingMetadata(
description = "Optional flag",
type = DataType.Primitive(PrimitiveType.BOOLEAN)
)
)
)
val store = PluginSettingsStore(
settings = mapOf("endpoint" to JsonPrimitive("https://custom.test")),
globalParams = mapOf("region" to JsonPrimitive("eu"))
)

val context = lifecycleManager.createPluginContext(
pkg = pkg,
manifest = manifest,
overriddenSettings = store
)

assertEquals(store.resolveCustomSettings(manifest), context.settings)
kotlin.test.assertFalse(context.settings.containsKey("optionalFlag"))
kotlin.test.assertFalse(context.settings.containsKey("region"))
}

@Test
fun testSettingsCaching() = runTest {
val fileSystem = FakeFileSystem()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package org.wip.plugintoolkit.features.plugin.model

import kotlinx.serialization.json.JsonPrimitive
import org.wip.plugintoolkit.api.Capability
import org.wip.plugintoolkit.api.DataType
import org.wip.plugintoolkit.api.PluginInfo
import org.wip.plugintoolkit.api.PluginManifest
import org.wip.plugintoolkit.api.PrimitiveType
import org.wip.plugintoolkit.api.Requirements
import org.wip.plugintoolkit.api.SettingMetadata
import org.wip.plugintoolkit.features.plugin.utils.CapabilityLockStatus
import org.wip.plugintoolkit.features.plugin.utils.CapabilityLockUtils
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class PluginSettingDefaultsTest {
private val manifest = PluginManifest(
manifestVersion = "1",
plugin = PluginInfo("example", "Example", "1.0", "Example plugin"),
requirements = Requirements(128, 10),
settings = mapOf(
"endpoint" to SettingMetadata(
defaultValue = JsonPrimitive("https://example.test"),
description = "Endpoint",
type = DataType.Primitive(PrimitiveType.STRING)
),
"enabled" to SettingMetadata(
description = "Enabled",
type = DataType.Primitive(PrimitiveType.BOOLEAN)
)
)
)

@Test
fun `manifest defaults are available before a user saves settings`() {
val resolved = PluginSettingsStore().resolveCustomSettings(manifest)

assertEquals(JsonPrimitive("https://example.test"), resolved["endpoint"])
assertFalse(resolved.containsKey("enabled"))
}

@Test
fun `user values override defaults and global values are exposed separately`() {
val store = PluginSettingsStore(
settings = mapOf("endpoint" to JsonPrimitive("https://custom.test")),
globalParams = mapOf("region" to JsonPrimitive("eu"))
)

val custom = store.resolveCustomSettings(manifest)
val provided = store.resolveProvidedValues(manifest)

assertEquals(JsonPrimitive("https://custom.test"), custom["endpoint"])
assertFalse(custom.containsKey("enabled"))
assertEquals(JsonPrimitive("eu"), provided["region"])
}

@Test
fun `global parameters cannot shadow custom settings in custom setting resolution`() {
val store = PluginSettingsStore(
settings = mapOf("endpoint" to JsonPrimitive("https://custom.test")),
globalParams = mapOf("endpoint" to JsonPrimitive("global-collision"))
)

assertEquals(JsonPrimitive("https://custom.test"), store.resolveCustomSettings(manifest)["endpoint"])
assertEquals(JsonPrimitive("global-collision"), store.resolveProvidedValues(manifest)["endpoint"])
}

@Test
fun `manifest defaults unlock capability gates before settings are persisted`() {
val capability = Capability(
name = "call",
description = "Call the configured endpoint",
returnType = DataType.Primitive(PrimitiveType.STRING),
requiresSettings = listOf("endpoint")
)
val provided = PluginSettingsStore().resolveProvidedValues(manifest)

assertTrue(capability.isReady(provided, manifest.settings))
assertTrue(
CapabilityLockUtils.checkCapabilityLockStatus(capability, emptyMap(), provided) is
CapabilityLockStatus.Unlocked
)
}
}