@forsion/tangu-computer-use 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +223 -0
- package/LICENSE +26 -0
- package/LICENSE.upstream +21 -0
- package/README.md +169 -0
- package/UPSTREAM.md +277 -0
- package/check.mjs +369 -0
- package/icon.png +0 -0
- package/install.sh +112 -0
- package/main.js +541 -0
- package/manifest.json +47 -0
- package/native/linux/bridge-rs/Cargo.lock +1204 -0
- package/native/linux/bridge-rs/Cargo.toml +18 -0
- package/native/linux/bridge-rs/src/atspi.rs +640 -0
- package/native/linux/bridge-rs/src/error.rs +65 -0
- package/native/linux/bridge-rs/src/lib.rs +11 -0
- package/native/linux/bridge-rs/src/main.rs +1074 -0
- package/native/linux/bridge-rs/src/protocol.rs +49 -0
- package/native/linux/bridge-rs/src/state.rs +293 -0
- package/native/linux/bridge-rs/src/wayland.rs +89 -0
- package/native/linux/bridge-rs/src/x11.rs +909 -0
- package/native/linux/bridge-rs/tests/protocol_tests.rs +73 -0
- package/native/macos/agent_cursor.swift +184 -0
- package/native/macos/agent_cursor_motion.swift +252 -0
- package/native/macos/agent_cursor_tests.swift +128 -0
- package/native/macos/agent_highlight.swift +229 -0
- package/native/macos/agent_highlight_tests.swift +99 -0
- package/native/macos/bridge.swift +3852 -0
- package/native/macos/foreground_activity.swift +57 -0
- package/native/macos/foreground_activity_tests.swift +31 -0
- package/native/macos/live_stream.swift +289 -0
- package/native/windows/bridge-rs/Cargo.lock +396 -0
- package/native/windows/bridge-rs/Cargo.toml +30 -0
- package/native/windows/bridge-rs/src/capture.rs +518 -0
- package/native/windows/bridge-rs/src/error.rs +81 -0
- package/native/windows/bridge-rs/src/input.rs +501 -0
- package/native/windows/bridge-rs/src/lib.rs +16 -0
- package/native/windows/bridge-rs/src/main.rs +1683 -0
- package/native/windows/bridge-rs/src/protocol.rs +53 -0
- package/native/windows/bridge-rs/src/refs.rs +237 -0
- package/native/windows/bridge-rs/src/state.rs +55 -0
- package/native/windows/bridge-rs/src/uia.rs +1252 -0
- package/native/windows/bridge-rs/src/window.rs +718 -0
- package/native/windows/bridge-rs/tests/protocol_tests.rs +252 -0
- package/native/windows/bridge-rs/tests/refs_tests.rs +86 -0
- package/native/windows/bridge-rs/tests/state_tests.rs +33 -0
- package/package.json +74 -0
- package/prebuilt/linux/arm64/linux-bridge +0 -0
- package/prebuilt/linux/x64/linux-bridge +0 -0
- package/prebuilt/macos/arm64/bridge +0 -0
- package/prebuilt/macos/arm64/tangu-computer-use.app.json +10 -0
- package/prebuilt/macos/arm64/tangu-computer-use.app.zip +0 -0
- package/prebuilt/macos/x64/bridge +0 -0
- package/prebuilt/macos/x64/tangu-computer-use.app.json +10 -0
- package/prebuilt/macos/x64/tangu-computer-use.app.zip +0 -0
- package/prebuilt/windows/windows-bridge.exe +0 -0
- package/scripts/blind-click.check.mjs +128 -0
- package/scripts/build-native.mjs +302 -0
- package/scripts/build.mjs +84 -0
- package/scripts/calc-fixture.mjs +78 -0
- package/scripts/cli-exit.check.mjs +49 -0
- package/scripts/helper-path.check.mjs +82 -0
- package/scripts/helper-refresh.check.mjs +62 -0
- package/scripts/helper-signal.check.mjs +59 -0
- package/scripts/highlight.check.mjs +32 -0
- package/scripts/keychain-free-install.check.mjs +140 -0
- package/scripts/live-view.check.mjs +124 -0
- package/scripts/macos-bundle.d.mts +1 -0
- package/scripts/macos-bundle.mjs +119 -0
- package/scripts/make-signing-cert.sh +57 -0
- package/scripts/mini-foreground.check.mjs +11 -0
- package/scripts/no-foreground.check.mjs +81 -0
- package/scripts/overlay-visible.check.mjs +221 -0
- package/scripts/package-macos-app.mjs +39 -0
- package/scripts/permissions.check.mjs +199 -0
- package/scripts/platform-contract.check.mjs +30 -0
- package/scripts/setup-helper.mjs +154 -0
- package/scripts/tangu-computer-use.entitlements +10 -0
- package/scripts/verify-macos-bundles.mjs +25 -0
- package/scripts/verify-package.mjs +74 -0
- package/skills/computer-use/SKILL.md +110 -0
- package/src/foregroundNote.ts +43 -0
- package/src/helperState.ts +31 -0
- package/src/index.ts +63 -0
- package/src/onboarding.ts +198 -0
- package/src/pi-compat.ts +49 -0
- package/src/settings.ts +11 -0
- package/src/setup.ts +97 -0
- package/src/tools.ts +289 -0
- package/src/vendor/actions.ts +130 -0
- package/src/vendor/bridge.ts +2405 -0
- package/src/vendor/cdp.ts +658 -0
- package/src/vendor/config.ts +113 -0
- package/src/vendor/contract.ts +104 -0
- package/src/vendor/note.ts +195 -0
- package/src/vendor/outline.ts +651 -0
- package/src/vendor/output.ts +134 -0
- package/src/vendor/permissions.ts +111 -0
- package/src/vendor/platform/architecture.ts +23 -0
- package/src/vendor/platform/coerce.ts +16 -0
- package/src/vendor/platform/index.ts +59 -0
- package/src/vendor/platform/linux/backend.ts +186 -0
- package/src/vendor/platform/linux/helper.ts +238 -0
- package/src/vendor/platform/macos/backend.ts +131 -0
- package/src/vendor/platform/macos/browser.ts +110 -0
- package/src/vendor/platform/macos/helper-path.d.mts +9 -0
- package/src/vendor/platform/macos/helper-path.mjs +34 -0
- package/src/vendor/platform/macos/helper.ts +291 -0
- package/src/vendor/platform/macos/permissions.ts +146 -0
- package/src/vendor/platform/types.ts +222 -0
- package/src/vendor/platform/windows/backend.ts +142 -0
- package/src/vendor/platform/windows/helper.ts +140 -0
- package/src/vendor/root-selection.ts +25 -0
- package/src/vendor/runtime.ts +129 -0
- package/src/vendor/state.ts +146 -0
- package/src/vendor/view.ts +147 -0
- package/tangu-plugins/computer-use/dist/foregroundNote.js +19 -0
- package/tangu-plugins/computer-use/dist/index.js +5672 -0
- package/tangu-plugins/computer-use/tangu-plugin.json +9 -0
- package/tsconfig.json +22 -0
- package/types/tangu-agent.d.ts +142 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
use serde_json::Value;
|
|
3
|
+
|
|
4
|
+
use crate::error::ProtocolError;
|
|
5
|
+
|
|
6
|
+
pub const PROTOCOL_VERSION: u32 = 4;
|
|
7
|
+
|
|
8
|
+
/// A request envelope received from the TypeScript host.
|
|
9
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
10
|
+
pub struct Request {
|
|
11
|
+
#[serde(rename = "protocolVersion")]
|
|
12
|
+
pub protocol_version: u32,
|
|
13
|
+
pub id: String,
|
|
14
|
+
pub cmd: String,
|
|
15
|
+
pub args: Value,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// A JSON response sent back to the TypeScript host.
|
|
19
|
+
#[derive(Debug, Clone, Serialize)]
|
|
20
|
+
pub struct Response {
|
|
21
|
+
#[serde(rename = "protocolVersion")]
|
|
22
|
+
pub protocol_version: u32,
|
|
23
|
+
pub id: String,
|
|
24
|
+
pub ok: bool,
|
|
25
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
26
|
+
pub result: Option<Value>,
|
|
27
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
28
|
+
pub error: Option<ProtocolError>,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
impl Response {
|
|
32
|
+
/// Build a success response.
|
|
33
|
+
pub fn ok(id: &str, result: Value) -> Self {
|
|
34
|
+
Self {
|
|
35
|
+
protocol_version: PROTOCOL_VERSION,
|
|
36
|
+
id: id.to_owned(),
|
|
37
|
+
ok: true,
|
|
38
|
+
result: Some(result),
|
|
39
|
+
error: None,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Build an error response.
|
|
44
|
+
pub fn err(id: &str, error: ProtocolError) -> Self {
|
|
45
|
+
Self {
|
|
46
|
+
protocol_version: PROTOCOL_VERSION,
|
|
47
|
+
id: id.to_owned(),
|
|
48
|
+
ok: false,
|
|
49
|
+
result: None,
|
|
50
|
+
error: Some(error),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
//! Window and element reference store.
|
|
2
|
+
//!
|
|
3
|
+
//! Maps monotonically increasing display-form references (`@w1`, `@e2`, …)
|
|
4
|
+
//! to opaque native handles (HWND, UIA element pointer). Each `State`
|
|
5
|
+
//! should get its own `RefStore` so that references remain scoped to a
|
|
6
|
+
//! single discovery session.
|
|
7
|
+
|
|
8
|
+
use std::collections::{HashMap, HashSet};
|
|
9
|
+
use std::fmt;
|
|
10
|
+
|
|
11
|
+
/// An opaque native handle (e.g., HWND or UIA element pointer).
|
|
12
|
+
///
|
|
13
|
+
/// Kept inside helper memory; never serialized to JSON.
|
|
14
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
15
|
+
pub struct NativeHandle(isize);
|
|
16
|
+
|
|
17
|
+
impl NativeHandle {
|
|
18
|
+
/// Wrap a raw handle value.
|
|
19
|
+
pub fn new(raw: isize) -> Self {
|
|
20
|
+
Self(raw)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Return the raw handle value.
|
|
24
|
+
pub fn raw(self) -> isize {
|
|
25
|
+
self.0
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// WindowRef
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/// A reference to a discovered window.
|
|
34
|
+
///
|
|
35
|
+
/// Display form is `@wN` where `N` is a monotonically increasing,
|
|
36
|
+
/// 1-indexed integer scoped to a `RefStore`.
|
|
37
|
+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
38
|
+
pub struct WindowRef {
|
|
39
|
+
pub(crate) id: u64,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
impl fmt::Display for WindowRef {
|
|
43
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
44
|
+
write!(f, "@w{}", self.id)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
impl WindowRef {
|
|
49
|
+
/// Parse a display-form window reference (e.g. `"@w1"`) into a `WindowRef`.
|
|
50
|
+
///
|
|
51
|
+
/// Returns `None` if the string does not match the expected pattern.
|
|
52
|
+
pub fn parse(s: &str) -> Option<Self> {
|
|
53
|
+
let s = s.trim();
|
|
54
|
+
if !s.starts_with("@w") {
|
|
55
|
+
return None;
|
|
56
|
+
}
|
|
57
|
+
let num_part = &s[2..];
|
|
58
|
+
let id: u64 = num_part.parse().ok()?;
|
|
59
|
+
Some(WindowRef { id })
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// ElementRef
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
/// A reference to a discovered UIA element.
|
|
68
|
+
///
|
|
69
|
+
/// Display form is `@eN` where `N` is a monotonically increasing,
|
|
70
|
+
/// 1-indexed integer scoped to a `RefStore`.
|
|
71
|
+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
72
|
+
pub struct ElementRef {
|
|
73
|
+
pub(crate) id: u64,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
impl fmt::Display for ElementRef {
|
|
77
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
78
|
+
write!(f, "@e{}", self.id)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// RefStore
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/// An owned store of window and element native handles.
|
|
87
|
+
///
|
|
88
|
+
/// Assigns monotonically increasing, 1-indexed references. Each
|
|
89
|
+
/// discovery state should hold its own `RefStore` so that reference
|
|
90
|
+
/// numbering is scoped to that state.
|
|
91
|
+
#[derive(Debug, Clone)]
|
|
92
|
+
pub struct RefStore {
|
|
93
|
+
next_window_id: u64,
|
|
94
|
+
next_element_id: u64,
|
|
95
|
+
windows: HashMap<u64, NativeHandle>,
|
|
96
|
+
window_ids: HashMap<NativeHandle, u64>,
|
|
97
|
+
elements: HashMap<u64, NativeHandle>,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
impl RefStore {
|
|
101
|
+
/// Create an empty store with counters starting at 1.
|
|
102
|
+
pub fn new() -> Self {
|
|
103
|
+
Self {
|
|
104
|
+
next_window_id: 1,
|
|
105
|
+
next_element_id: 1,
|
|
106
|
+
windows: HashMap::new(),
|
|
107
|
+
window_ids: HashMap::new(),
|
|
108
|
+
elements: HashMap::new(),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// Insert a window native handle and return its `WindowRef`.
|
|
113
|
+
pub fn insert_window(&mut self, handle: NativeHandle) -> WindowRef {
|
|
114
|
+
if let Some(id) = self.window_ids.get(&handle).copied() {
|
|
115
|
+
return WindowRef { id };
|
|
116
|
+
}
|
|
117
|
+
let id = self.next_window_id;
|
|
118
|
+
self.next_window_id += 1;
|
|
119
|
+
self.windows.insert(id, handle);
|
|
120
|
+
self.window_ids.insert(handle, id);
|
|
121
|
+
WindowRef { id }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Insert a UIA element native handle and return its `ElementRef`.
|
|
125
|
+
pub fn insert_element(&mut self, handle: NativeHandle) -> ElementRef {
|
|
126
|
+
let id = self.next_element_id;
|
|
127
|
+
self.next_element_id += 1;
|
|
128
|
+
self.elements.insert(id, handle);
|
|
129
|
+
ElementRef { id }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// Look up the native handle for a previously-returned `WindowRef`.
|
|
133
|
+
///
|
|
134
|
+
/// Returns `None` if the ref did not originate from this store.
|
|
135
|
+
pub fn get_window(&self, wref: &WindowRef) -> Option<NativeHandle> {
|
|
136
|
+
self.windows.get(&wref.id).copied()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// Look up the native handle for a previously-returned `ElementRef`.
|
|
140
|
+
///
|
|
141
|
+
/// Returns `None` if the ref did not originate from this store.
|
|
142
|
+
pub fn get_element(&self, eref: &ElementRef) -> Option<NativeHandle> {
|
|
143
|
+
self.elements.get(&eref.id).copied()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Keep only the currently discoverable windows while preserving refs for
|
|
147
|
+
/// HWNDs that remain live. This makes repeated discovery stable and bounded.
|
|
148
|
+
pub fn retain_window_refs<'a>(&mut self, refs: impl IntoIterator<Item = &'a str>) {
|
|
149
|
+
let live = refs
|
|
150
|
+
.into_iter()
|
|
151
|
+
.filter_map(WindowRef::parse)
|
|
152
|
+
.map(|reference| reference.id)
|
|
153
|
+
.collect::<HashSet<_>>();
|
|
154
|
+
self.windows.retain(|id, _| live.contains(id));
|
|
155
|
+
self.window_ids.retain(|_, id| live.contains(id));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
impl Default for RefStore {
|
|
160
|
+
fn default() -> Self {
|
|
161
|
+
Self::new()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Tests
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
#[cfg(test)]
|
|
170
|
+
mod unit_tests {
|
|
171
|
+
use super::*;
|
|
172
|
+
|
|
173
|
+
#[test]
|
|
174
|
+
fn window_ref_display() {
|
|
175
|
+
let w = WindowRef { id: 1 };
|
|
176
|
+
assert_eq!(w.to_string(), "@w1");
|
|
177
|
+
let w = WindowRef { id: 42 };
|
|
178
|
+
assert_eq!(w.to_string(), "@w42");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#[test]
|
|
182
|
+
fn element_ref_display() {
|
|
183
|
+
let e = ElementRef { id: 1 };
|
|
184
|
+
assert_eq!(e.to_string(), "@e1");
|
|
185
|
+
let e = ElementRef { id: 7 };
|
|
186
|
+
assert_eq!(e.to_string(), "@e7");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#[test]
|
|
190
|
+
fn native_handle_roundtrip() {
|
|
191
|
+
let h = NativeHandle::new(0xBAD);
|
|
192
|
+
assert_eq!(h.raw(), 0xBAD);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#[test]
|
|
196
|
+
fn window_ref_parse_valid() {
|
|
197
|
+
let w = WindowRef::parse("@w1").expect("should parse @w1");
|
|
198
|
+
assert_eq!(w.to_string(), "@w1");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#[test]
|
|
202
|
+
fn window_ref_parse_valid_high_number() {
|
|
203
|
+
let w = WindowRef::parse("@w42").expect("should parse @w42");
|
|
204
|
+
assert_eq!(w.to_string(), "@w42");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#[test]
|
|
208
|
+
fn window_ref_parse_invalid_no_at() {
|
|
209
|
+
assert!(WindowRef::parse("w1").is_none());
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#[test]
|
|
213
|
+
fn window_ref_parse_invalid_wrong_prefix() {
|
|
214
|
+
assert!(WindowRef::parse("@x1").is_none());
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[test]
|
|
218
|
+
fn window_ref_parse_invalid_empty() {
|
|
219
|
+
assert!(WindowRef::parse("").is_none());
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#[test]
|
|
223
|
+
fn window_ref_parse_invalid_non_numeric() {
|
|
224
|
+
assert!(WindowRef::parse("@wabc").is_none());
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#[test]
|
|
228
|
+
fn window_ref_parse_roundtrip() {
|
|
229
|
+
// Verify that display-then-parse recovers the original id.
|
|
230
|
+
for id in &[1u64, 5, 100] {
|
|
231
|
+
let w = WindowRef { id: *id };
|
|
232
|
+
let s = w.to_string();
|
|
233
|
+
let parsed = WindowRef::parse(&s).expect("roundtrip parse");
|
|
234
|
+
assert_eq!(parsed.id, *id);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//! State identifier generation.
|
|
2
|
+
//!
|
|
3
|
+
//! Provides monotonically increasing, globally unique state IDs used
|
|
4
|
+
//! to scope discovery sessions and ref stores.
|
|
5
|
+
|
|
6
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
7
|
+
|
|
8
|
+
/// Generates unique, monotonically increasing state identifiers.
|
|
9
|
+
///
|
|
10
|
+
/// Each call to `fresh(prefix)` returns a string of the form
|
|
11
|
+
/// `{prefix}-{N}` where `N` is a global counter that never repeats
|
|
12
|
+
/// within the lifetime of the helper process.
|
|
13
|
+
pub struct StateId;
|
|
14
|
+
|
|
15
|
+
impl StateId {
|
|
16
|
+
/// Return a new unique identifier starting with `prefix`.
|
|
17
|
+
///
|
|
18
|
+
/// # Examples
|
|
19
|
+
///
|
|
20
|
+
/// ```
|
|
21
|
+
/// use windows_bridge::state::StateId;
|
|
22
|
+
///
|
|
23
|
+
/// let id_a = StateId::fresh("session");
|
|
24
|
+
/// let id_b = StateId::fresh("session");
|
|
25
|
+
/// assert_ne!(id_a, id_b);
|
|
26
|
+
/// assert!(id_a.starts_with("session-"));
|
|
27
|
+
/// ```
|
|
28
|
+
pub fn fresh(prefix: &str) -> String {
|
|
29
|
+
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
30
|
+
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
|
|
31
|
+
format!("{prefix}-{n}")
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Tests
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
#[cfg(test)]
|
|
40
|
+
mod unit_tests {
|
|
41
|
+
use super::*;
|
|
42
|
+
|
|
43
|
+
#[test]
|
|
44
|
+
fn consecutive_ids_differ() {
|
|
45
|
+
let a = StateId::fresh("test");
|
|
46
|
+
let b = StateId::fresh("test");
|
|
47
|
+
assert_ne!(a, b);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#[test]
|
|
51
|
+
fn id_contains_prefix() {
|
|
52
|
+
let id = StateId::fresh("foobar");
|
|
53
|
+
assert!(id.starts_with("foobar-"));
|
|
54
|
+
}
|
|
55
|
+
}
|