Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
691437a
feat(orchestra): derive the flow-command schema from the parser's own…
amanjeetsingh150 Sep 3, 2026
653c81e
fix(orchestra): only call an argument required when the parser cannot…
amanjeetsingh150 Sep 3, 2026
1d94d87
Revert the publish-schemas workflow deletion
amanjeetsingh150 Sep 3, 2026
1f233bc
feat(orchestra): let a String YAML field declare the vocabulary it is…
amanjeetsingh150 Sep 3, 2026
a65fa4d
test(orchestra): pin the schema against commands being added and changed
amanjeetsingh150 Sep 4, 2026
2f48576
test(orchestra): make the schema-vs-Jackson check actually able to fail
amanjeetsingh150 Sep 4, 2026
ab0901e
test(orchestra): check the one-of rule against the parser, and say wh…
amanjeetsingh150 Sep 4, 2026
e44a632
fix(orchestra): keep the YAML spelling off the MaestroCommand wire
amanjeetsingh150 Sep 4, 2026
883a861
fix(orchestra): publish what the parser actually accepts for selector…
amanjeetsingh150 Sep 4, 2026
33f9c23
feat(orchestra): version the schema document and fix its ordering
amanjeetsingh150 Sep 4, 2026
29e0df0
test(orchestra): check the schema's hand-written claims, and correct …
amanjeetsingh150 Sep 4, 2026
9580c98
feat(orchestra): give `action` the vocabulary it has always had
amanjeetsingh150 Sep 4, 2026
4a9afd2
docs(orchestra): correct the setDarkMode/setAirplaneMode vocabulary c…
amanjeetsingh150 Sep 4, 2026
a8f44a7
feat(orchestra): publish swipe's alternative shapes under YAML names
amanjeetsingh150 Sep 4, 2026
c9a29a4
test(orchestra): check the schema against the flows in e2e
amanjeetsingh150 Sep 4, 2026
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
8 changes: 8 additions & 0 deletions maestro-client/src/main/java/maestro/KeyCode.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package maestro

/**
* A key `pressKey` can press. [description] is the word written in YAML, matched case-insensitively by
* [getByName]; the schema derived from the parser reads it through `@YamlValues(spelledBy = "description")`.
*
* The spelling deliberately does NOT live in a `@JsonProperty` on each constant. Jackson serializes this
* enum as `PressKeyCommand.code` on the MaestroCommand wire, where the constant name is what is written
* and read back, so renaming it there would break every command already persisted or in flight.
*/
enum class KeyCode(
val description: String,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1177,9 +1177,15 @@ data class StopRecordingCommand(
}
}

enum class AirplaneValue {
Enable,
Disable,
/**
* [yamlValue] is the word written in YAML. It is deliberately NOT a `@JsonProperty` on each constant:
* Jackson serializes this enum as `SetAirplaneModeCommand.value` on the MaestroCommand wire, where the
* constant name is what is written and read back, so renaming it there would break every command already
* persisted or in flight. The schema reads the word through `@YamlValues(spelledBy = "yamlValue")`.
*/
enum class AirplaneValue(val yamlValue: String) {
Enable("enabled"),
Disable("disabled"),
}

data class SetAirplaneModeCommand(
Expand Down Expand Up @@ -1210,9 +1216,15 @@ data class ToggleAirplaneModeCommand(
}
}

enum class DarkModeValue {
Enable,
Disable,
/**
* [yamlValue] is the word written in YAML. It is deliberately NOT a `@JsonProperty` on each constant:
* Jackson serializes this enum as `SetDarkModeCommand.value` on the MaestroCommand wire, where the
* constant name is what is written and read back, so renaming it there would break every command already
* persisted or in flight. The schema reads the word through `@YamlValues(spelledBy = "yamlValue")`.
*/
enum class DarkModeValue(val yamlValue: String) {
Enable("enabled"),
Disable("disabled"),
}

data class SetDarkModeCommand(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,11 @@ private fun String.indentWidth(): Int {

// Each lambda receives a YamlFluentCommand pre-populated with sourceInfo, and
// fills in the matching command field via copy().
private val stringCommands = mapOf<String, (YamlFluentCommand) -> YamlFluentCommand>(
//
// Internal rather than private: this map is the only record of which commands may be written as a
// bare string (`- back`), and maestro.orchestra.yaml.schema.FlowCommandSchema reads its keys so the
// published schema cannot drift from what the parser accepts.
internal val stringCommands = mapOf<String, (YamlFluentCommand) -> YamlFluentCommand>(
"launchApp" to { it.copy(launchApp = YamlLaunchApp(
appId = null,
clearState = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package maestro.orchestra.yaml

import com.fasterxml.jackson.annotation.JsonCreator
import maestro.orchestra.yaml.schema.YamlRequiresOneOf

@YamlRequiresOneOf("files")
data class YamlAddMedia(
val files: List<String?>? = null,
val label: String? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
package maestro.orchestra.yaml

import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import maestro.orchestra.ElementTrait
import maestro.orchestra.yaml.schema.YamlValues

@JsonDeserialize(`as` = YamlElementSelector::class)
data class YamlElementSelector(
Expand All @@ -40,6 +42,8 @@ data class YamlElementSelector(
val rightOf: YamlElementSelectorUnion? = null,
val containsChild: YamlElementSelectorUnion? = null,
val containsDescendants: List<YamlElementSelectorUnion>? = null,
/** One or more trait names separated by spaces; each is looked up in [ElementTrait]. */
@YamlValues(ElementTrait::class)
val traits: String? = null,
val index: String? = null,
val enabled: Boolean? = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package maestro.orchestra.yaml

import maestro.orchestra.yaml.schema.YamlRequiresOneOf

@YamlRequiresOneOf("visible", "notVisible")
data class YamlExtendedWaitUntil(
val visible: YamlElementSelectorUnion? = null,
val notVisible: YamlElementSelectorUnion? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import maestro.orchestra.ToggleDarkModeCommand
import maestro.orchestra.TravelCommand
import maestro.orchestra.WaitForAnimationToEndCommand
import maestro.orchestra.error.InvalidFlowFile
import maestro.orchestra.yaml.schema.YamlValues
import maestro.orchestra.error.MediaFileNotFound
import maestro.orchestra.error.SyntaxError
import maestro.orchestra.util.Env.withEnv
Expand Down Expand Up @@ -119,9 +120,9 @@ data class YamlFluentCommand(
val setPermissions: YamlSetPermissions? = null,
val swipe: YamlSwipe? = null,
val openLink: YamlOpenLink? = null,
val openBrowser: String? = null,
val pressKey: YamlPressKey? = null,
val eraseText: YamlEraseText? = null,
@YamlValues(YamlNavigationAction::class, spelledBy = "yamlValue")
val action: String? = null,
val takeScreenshot: YamlTakeScreenshot? = null,
val extendedWaitUntil: YamlExtendedWaitUntil? = null,
Expand Down Expand Up @@ -284,13 +285,13 @@ data class YamlFluentCommand(

eraseText != null -> listOf(eraseCommand(eraseText))
action != null -> listOf(
when (action) {
"back" -> MaestroCommand(BackPressCommand())
"hideKeyboard" -> MaestroCommand(HideKeyboardCommand())
"scroll" -> MaestroCommand(ScrollCommand())
"clearKeychain" -> MaestroCommand(ClearKeychainCommand())
"pasteText" -> MaestroCommand(PasteTextCommand())
else -> error("Unknown navigation target: $action")
when (YamlNavigationAction.entries.firstOrNull { it.yamlValue == action }) {
YamlNavigationAction.Back -> MaestroCommand(BackPressCommand())
YamlNavigationAction.HideKeyboard -> MaestroCommand(HideKeyboardCommand())
YamlNavigationAction.Scroll -> MaestroCommand(ScrollCommand())
YamlNavigationAction.ClearKeychain -> MaestroCommand(ClearKeychainCommand())
YamlNavigationAction.PasteText -> MaestroCommand(PasteTextCommand())
null -> error("Unknown navigation target: $action")
}
)

Expand Down Expand Up @@ -921,12 +922,6 @@ data class YamlFluentCommand(
}

is YamlSwipeElement -> return swipeElementCommand(swipe)
else -> {
throw IllegalStateException(
"Provide swipe direction UP, DOWN, RIGHT OR LEFT or by giving explicit " +
"start and end coordinates."
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package maestro.orchestra.yaml

/**
* The words `action` accepts. `action` is a legacy spelling of commands that all exist under their own
* names — `action: back` is `- back` — and it is a plain `String` so that it keeps parsing as one, but
* the set of words it takes is closed. Holding them here rather than as literals in a `when` is what
* lets the schema advertise them; [yamlValue] is the word, the constant name is not on any wire.
*/
enum class YamlNavigationAction(val yamlValue: String) {
Back("back"),
HideKeyboard("hideKeyboard"),
Scroll("scroll"),
ClearKeychain("clearKeychain"),
PasteText("pasteText"),
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package maestro.orchestra.yaml

import com.fasterxml.jackson.annotation.JsonCreator
import maestro.KeyCode
import maestro.orchestra.yaml.schema.YamlValues

data class YamlPressKey (
@YamlValues(KeyCode::class, spelledBy = "description")
val key: String,
val label: String? = null,
val optional: Boolean = false,
){
companion object {
@JvmStatic
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
fun parse(key: String) = YamlPressKey(
fun parse(@YamlValues(KeyCode::class, spelledBy = "description") key: String) = YamlPressKey(
key = key,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package maestro.orchestra.yaml

import maestro.orchestra.yaml.schema.YamlRequiresOneOf

@YamlRequiresOneOf("file", "commands", exclusive = true)
data class YamlRetryCommand(
val maxRetries: String? = null,
val file: String? = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package maestro.orchestra.yaml

import com.fasterxml.jackson.annotation.JsonCreator
import maestro.orchestra.yaml.schema.YamlRequiresOneOf

@YamlRequiresOneOf("file", "commands", exclusive = true)
data class YamlRunFlow(
val file: String? = null,
val `when`: YamlCondition? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,61 @@ import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.node.TextNode
import maestro.orchestra.AirplaneValue
import maestro.orchestra.yaml.schema.YamlValues

@JsonDeserialize(using = YamlSetAirplaneModeDeserializer::class)
data class YamlSetAirplaneMode(
@YamlValues(AirplaneValue::class, spelledBy = "yamlValue")
val value: AirplaneValue,
val label: String? = null,
val optional: Boolean = false,
) {
companion object {
@JvmStatic
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
fun parse(value: AirplaneValue): YamlSetAirplaneMode {
fun parse(@YamlValues(AirplaneValue::class, spelledBy = "yamlValue") value: AirplaneValue): YamlSetAirplaneMode {
return YamlSetAirplaneMode(value)
}
}
}

/**
* Accepts `setAirplaneMode: enabled` and the object form carrying `value`, `label` and `optional`.
*
* The accepted vocabulary is not spelled out here: it lives on [AirplaneValue] as `yamlValue`, read both by
* this deserializer and by the schema derived from these types, so the two cannot disagree. It is
* deliberately not a `@JsonProperty` on each constant -- that name is the MaestroCommand wire format.
*/
class YamlSetAirplaneModeDeserializer : JsonDeserializer<YamlSetAirplaneMode>() {

override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): YamlSetAirplaneMode {
val mapper = (parser.codec as ObjectMapper)
val mapper = parser.codec as ObjectMapper
val root: TreeNode = mapper.readTree(parser)
val input = root.fieldNames().asSequence().toList()
val label = getLabel(root)
when {
input.contains("value") -> {
val parsedValue = root.get("value").toString().replace("\"", "")
val returnValue = when (parsedValue) {
"enabled" -> AirplaneValue.Enable
"disabled" -> AirplaneValue.Disable
else -> throwInvalidInputException(input)
}
return YamlSetAirplaneMode(returnValue, label)
}
(root.isValueNode && root.toString().contains("enabled")) -> {
return YamlSetAirplaneMode(AirplaneValue.Enable, label)
}
(root.isValueNode && root.toString().contains("disabled")) -> {
return YamlSetAirplaneMode(AirplaneValue.Disable, label)
}
else -> throwInvalidInputException(input)

if (root.isValueNode) {
return YamlSetAirplaneMode(toAirplaneValue(root))
}

val valueNode = root.get("value")
?: throwInvalidInputException(root.fieldNames().asSequence().toList())

return YamlSetAirplaneMode(
value = toAirplaneValue(valueNode),
label = root.get("label")?.let { mapper.convertValue(it, String::class.java) },
optional = root.get("optional")?.let { mapper.convertValue(it, Boolean::class.java) } ?: false,
)
}

/**
* Looks the word up on [AirplaneValue] rather than letting Jackson convert it, so the constant names stay
* the MaestroCommand wire format while YAML keeps its own spelling. Still derived from the enum, so
* the parser and the schema cannot disagree.
*/
private fun toAirplaneValue(node: TreeNode): AirplaneValue {
val text = (node as? TextNode)?.textValue() ?: throwInvalidInputException(listOf(node.toString()))
return AirplaneValue.entries.firstOrNull { it.yamlValue == text } ?: throwInvalidInputException(listOf(text))
}

private fun throwInvalidInputException(input: List<String>): Nothing {
Expand All @@ -60,13 +73,4 @@ class YamlSetAirplaneModeDeserializer : JsonDeserializer<YamlSetAirplaneMode>()
"It seems you provided invalid input with: $input"
)
}

private fun getLabel(root: TreeNode): String? {
return if (root.path("label").isMissingNode) {
null
} else {
root.path("label").toString().replace("\"", "")
}
}

}
Loading
Loading