Skip to main content

agentmux_srv/server/service/
window_query.rs

1// Copyright 2025-2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! `window` service handlers — read-only lookups (`GetWindow`,
5//! `FindWindowByLabel`). Split out of `window.rs`; see that file's
6//! dispatcher for the full method list.
7
8use crate::backend::obj::*;
9use crate::backend::service::{self, WebCallType, WebReturnType};
10
11use super::super::AppState;
12
13pub(crate) async fn handle_get_window(state: &AppState, call: &WebCallType) -> WebReturnType {
14    let store = &state.mstore;
15    let args = &call.args;
16    let window_id: String = match service::get_arg(args, 0) {
17        Ok(v) => v,
18        Err(e) => return WebReturnType::error(e),
19    };
20    match store.must_get::<Window>(&window_id) {
21        Ok(win) => WebReturnType::success(serde_json::to_value(&win).unwrap_or_default()),
22        Err(e) => WebReturnType::error(e.to_string()),
23    }
24}
25
26// SPEC_POOL_ADOPTION_AND_WINDOW_ROW_CRUMB_2026_07_11 Residual 2 —
27// resolve Window rows by the `host:label` meta crumb written at
28// CreateWindow time. Returns ALL matching window ids (labels can
29// recur across host restarts — the crumb is a hint, not an
30// identity); the caller decides what a multi-match means for its
31// use case. An empty array is a normal answer (row predates the
32// crumb, or the label never created a row).
33pub(crate) async fn handle_find_window_by_label(state: &AppState, call: &WebCallType) -> WebReturnType {
34    let store = &state.mstore;
35    let args = &call.args;
36    let label: String = match service::get_arg(args, 0) {
37        Ok(v) => v,
38        Err(e) => return WebReturnType::error(e),
39    };
40    if label.is_empty() {
41        return WebReturnType::error("FindWindowByLabel: empty label".to_string());
42    }
43    match store.get_all::<Window>() {
44        Ok(wins) => {
45            let ids: Vec<String> = wins
46                .into_iter()
47                .filter(|w| {
48                    w.meta.get("host:label").and_then(|v| v.as_str())
49                        == Some(label.as_str())
50                })
51                .map(|w| w.oid)
52                .collect();
53            WebReturnType::success(serde_json::json!(ids))
54        }
55        Err(e) => WebReturnType::error(e.to_string()),
56    }
57}
58
59#[cfg(test)]
60mod window_label_crumb_tests {
61    use super::super::window::handle_window_service;
62    use crate::backend::obj::Window;
63    use crate::backend::service::WebCallType;
64    use crate::server::tests::test_state;
65
66    fn create_call(workspace_id: &str, label: Option<&str>) -> WebCallType {
67        let mut args = vec![
68            serde_json::Value::Null,
69            serde_json::Value::String(workspace_id.to_string()),
70        ];
71        if let Some(l) = label {
72            args.push(serde_json::Value::String(l.to_string()));
73        }
74        WebCallType {
75            service: "window".to_string(),
76            method: "CreateWindow".to_string(),
77            uicontext: None,
78            args,
79        }
80    }
81
82    fn find_call(label: &str) -> WebCallType {
83        WebCallType {
84            service: "window".to_string(),
85            method: "FindWindowByLabel".to_string(),
86            uicontext: None,
87            args: vec![serde_json::Value::String(label.to_string())],
88        }
89    }
90
91    /// The crumb round-trip this exists for: CreateWindow with a label arg
92    /// persists `host:label` on the row, and FindWindowByLabel resolves it
93    /// back to exactly that window id — no host-side registration involved.
94    #[tokio::test]
95    async fn crumb_written_at_create_and_resolvable_by_label() {
96        let state = test_state();
97        let ret = handle_window_service(
98            &state,
99            &create_call("", Some("window-pool-crumbtest1")),
100        )
101        .await;
102        assert!(ret.success, "CreateWindow failed: {:?}", ret.error);
103        let created_id = ret
104            .data
105            .as_ref()
106            .and_then(|d| d.get("oid"))
107            .and_then(|v| v.as_str())
108            .expect("CreateWindow returns the Window with oid")
109            .to_string();
110
111        // The crumb is on the persisted row itself.
112        let row = state
113            .mstore
114            .must_get::<Window>(&created_id)
115            .expect("created window row exists");
116        assert_eq!(
117            row.meta.get("host:label").and_then(|v| v.as_str()),
118            Some("window-pool-crumbtest1"),
119            "host:label crumb missing from the Window row meta"
120        );
121
122        // And resolvable through the read RPC.
123        let found = handle_window_service(&state, &find_call("window-pool-crumbtest1")).await;
124        assert!(found.success, "FindWindowByLabel failed: {:?}", found.error);
125        let ids: Vec<String> =
126            serde_json::from_value(found.data.expect("find returns ids")).unwrap();
127        assert_eq!(ids, vec![created_id]);
128    }
129
130    /// Old two-arg callers keep working and write no crumb — the arg is
131    /// genuinely optional, not silently defaulted to something matchable.
132    #[tokio::test]
133    async fn two_arg_create_window_writes_no_crumb() {
134        let state = test_state();
135        let ret = handle_window_service(&state, &create_call("", None)).await;
136        assert!(ret.success, "CreateWindow failed: {:?}", ret.error);
137        let created_id = ret
138            .data
139            .as_ref()
140            .and_then(|d| d.get("oid"))
141            .and_then(|v| v.as_str())
142            .unwrap()
143            .to_string();
144        let row = state.mstore.must_get::<Window>(&created_id).unwrap();
145        assert!(
146            !row.meta.contains_key("host:label"),
147            "two-arg CreateWindow must not invent a crumb"
148        );
149    }
150
151    /// A label nothing ever created is a normal empty answer, not an error
152    /// (rows predating the crumb look exactly like this to consumers).
153    #[tokio::test]
154    async fn unknown_label_returns_empty_not_error() {
155        let state = test_state();
156        let found = handle_window_service(&state, &find_call("window-never-existed")).await;
157        assert!(found.success);
158        let ids: Vec<String> =
159            serde_json::from_value(found.data.expect("find returns ids")).unwrap();
160        assert!(ids.is_empty());
161    }
162
163    /// Empty label is a caller bug — reject loudly rather than matching
164    /// every crumbless row's absent key semantics ambiguously.
165    #[tokio::test]
166    async fn empty_label_is_an_error() {
167        let state = test_state();
168        let found = handle_window_service(&state, &find_call("")).await;
169        assert!(!found.success);
170    }
171}