1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt;
// ── Enums ──────────────────────────────────────────────────────────────────
/// Condition to poll for in the `wait_for` tool.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WaitCondition {
/// Wait for text to appear in the page.
Text,
/// Wait for text to disappear from the page.
TextGone,
/// Wait for a CSS selector to match an element.
Selector,
/// Wait for a CSS selector to stop matching.
SelectorGone,
/// Wait for the URL to contain a substring.
Url,
/// Wait for all IPC calls to complete.
IpcIdle,
/// Wait for all network requests to complete.
NetworkIdle,
/// Poll a JavaScript expression until it is truthy (or equals `expected`).
///
/// The expression is evaluated in the webview each poll via the same engine
/// as `eval_js`, so it may `await` (e.g.
/// `(await window.__TAURI_INTERNALS__.invoke('get_status')).running === false`).
/// This is the level-triggered, race-free way to await a fire-and-forget
/// backend command that exposes a pollable status — no app changes required.
Expression,
/// Block until a named Tauri event fires on the app's event bus.
///
/// Edge-triggered completion: evaluated server-side against Victauri's
/// captured event bus, with a `since_ms` look-back so an event that fired in
/// the gap between `invoke_command` and this call is not missed. The app must
/// emit the event and Victauri must be configured to capture it via
/// `VictauriBuilder::listen_events(&["..."])` (window-lifecycle events are
/// captured automatically).
Event,
}
impl WaitCondition {
/// Returns the `snake_case` string for JS bridge consumption.
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Text => "text",
Self::TextGone => "text_gone",
Self::Selector => "selector",
Self::SelectorGone => "selector_gone",
Self::Url => "url",
Self::IpcIdle => "ipc_idle",
Self::NetworkIdle => "network_idle",
Self::Expression => "expression",
Self::Event => "event",
}
}
}
impl fmt::Display for WaitCondition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
// ── Intent ─────────────────────────────────────────────────────────────────
/// Parameters for the `resolve_command` tool.
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ResolveCommandParams {
/// Natural language query describing what you want to do (e.g. "save the user's settings").
pub query: String,
/// Maximum number of results to return. Default: 5.
pub limit: Option<usize>,
}
/// Parameters for the `semantic_assert` tool.
#[derive(Debug, Deserialize, JsonSchema)]
pub struct SemanticAssertParams {
/// JavaScript expression to evaluate in the webview. The result is checked against the assertion.
pub expression: String,
/// Human-readable label for this assertion (e.g. "user is logged in").
/// Optional — defaults to empty so a minimal `{expression, condition}` call
/// succeeds instead of failing deserialization with an opaque 400.
#[serde(default)]
pub label: String,
/// Condition to evaluate against the actual value.
pub condition: victauri_core::AssertionCondition,
/// Expected value for the assertion. Optional for truthy/falsy/exists conditions.
#[serde(default)]
pub expected: serde_json::Value,
/// Target webview label.
#[serde(alias = "window", alias = "window_label")]
pub webview_label: Option<String>,
}
// ── Wait ───────────────────────────────────────────────────────────────────
/// Parameters for the `wait_for` tool.
#[derive(Debug, Deserialize, JsonSchema)]
pub struct WaitForParams {
/// Condition to wait for.
pub condition: WaitCondition,
/// Value for the condition: text to find, CSS selector, URL substring,
/// JS expression (for `expression` — a single expression; a trailing `;` or comment is
/// fine, and one that does not parse fails at once instead of polling to the timeout),
/// or Tauri event name (for `event`).
pub value: Option<String>,
/// Maximum time to wait in milliseconds. Default: 10000; capped at 120000. `0` checks the
/// condition exactly once, immediately, and returns (`ok: false` if it is not met yet).
pub timeout_ms: Option<u64>,
/// Polling interval in milliseconds. Default: 200.
pub poll_ms: Option<u64>,
/// For the `expression` condition: the JSON value the expression must equal
/// to satisfy the wait. When omitted, the wait is satisfied as soon as the
/// expression evaluates to a truthy value.
#[serde(default)]
pub expected: Option<serde_json::Value>,
/// For the `event` condition: how far back (in milliseconds) to look for a
/// matching event when the wait begins, so an event that fired just before
/// this call is not missed. Default: 2000.
pub since_ms: Option<u64>,
/// Target webview label.
#[serde(alias = "window", alias = "window_label")]
pub webview_label: Option<String>,
}
// ── App State Probes ─────────────────────────────────────────────────────────
/// Parameters for the `app_state` tool.
#[derive(Debug, Deserialize, JsonSchema)]
pub struct AppStateParams {
/// Name of the probe to run. When omitted, lists all available probe names.
pub probe: Option<String>,
}
// ── Find Elements ──────────────────────────────────────────────────────────
/// Parameters for the `find_elements` tool.
#[derive(Debug, Deserialize, JsonSchema)]
pub struct FindElementsParams {
/// Text content to search for (case-insensitive substring match).
pub text: Option<String>,
/// ARIA role to match (exact match).
pub role: Option<String>,
/// data-testid attribute value to match (exact match).
pub test_id: Option<String>,
/// CSS selector to match (also accepts `selector` as an alias).
pub css: Option<String>,
/// Alias for `css` — CSS selector to match.
pub selector: Option<String>,
/// Accessible name to search for (aria-label, title, placeholder -- case-insensitive substring).
pub name: Option<String>,
/// Maximum number of results to return. Default: 10.
pub max_results: Option<u32>,
/// HTML tag name to match (e.g. "button", "input").
pub tag: Option<String>,
/// Placeholder text to match (case-insensitive substring).
pub placeholder: Option<String>,
/// Alt text to match (case-insensitive substring).
pub alt: Option<String>,
/// Title attribute to match (case-insensitive substring).
pub title_attr: Option<String>,
/// Associated label text to match (finds inputs by their label).
pub label: Option<String>,
/// If true, text matching is exact instead of substring.
pub exact: Option<bool>,
/// Filter by enabled state.
pub enabled: Option<bool>,
/// Target webview label.
#[serde(alias = "window", alias = "window_label")]
pub webview_label: Option<String>,
}
/// Parameters for the `get_diagnostics` tool.
#[derive(Debug, Deserialize, JsonSchema)]
pub struct DiagnosticsParams {
/// Target a specific webview window by label.
#[serde(alias = "window", alias = "window_label")]
pub webview_label: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn semantic_assert_params_label_is_optional() {
// Regression: `label` was a required `String`, so a minimal
// `{expression, condition}` call failed deserialization with an opaque
// 400. It is now `#[serde(default)]` and must default to empty.
let params: SemanticAssertParams = serde_json::from_value(serde_json::json!({
"expression": "1 + 1",
"condition": "equals",
"expected": 2
}))
.expect("minimal assert_semantic call (no label) must deserialize");
assert_eq!(params.label, "");
assert_eq!(params.expression, "1 + 1");
assert!(params.webview_label.is_none());
}
#[test]
fn semantic_assert_params_label_still_accepted() {
let params: SemanticAssertParams = serde_json::from_value(serde_json::json!({
"expression": "x",
"label": "user is logged in",
"condition": "truthy"
}))
.expect("explicit label must still deserialize");
assert_eq!(params.label, "user is logged in");
}
}