mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
feat(protocol): bump to 1.1 and redefine record traces as state-linked v3
Replace page-indexed record traces with states/steps + FillCommit/hover/scroll, and raise daemon/extension protocol floors together so old 1.0 peers are rejected.
This commit is contained in:
@@ -65,6 +65,11 @@ function deferredFakeTransport(): { transport: Transport; emit: (frame: Protocol
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("performHandshake", () => {
|
describe("performHandshake", () => {
|
||||||
|
it("advertises the Trace v3 compatibility boundary", () => {
|
||||||
|
expect(PROTOCOL_VERSION).toBe("1.1");
|
||||||
|
expect(MIN_COMPATIBLE_PROTOCOL).toBe("1.1");
|
||||||
|
});
|
||||||
|
|
||||||
it("sends system.handshake with identity and both compat fields", async () => {
|
it("sends system.handshake with identity and both compat fields", async () => {
|
||||||
let sentFrame: ProtocolFrame | null = null;
|
let sentFrame: ProtocolFrame | null = null;
|
||||||
const transport = fakeTransport((req) => {
|
const transport = fakeTransport((req) => {
|
||||||
@@ -74,9 +79,9 @@ describe("performHandshake", () => {
|
|||||||
result: {
|
result: {
|
||||||
server: "browser-skill-daemon",
|
server: "browser-skill-daemon",
|
||||||
version: "0.1.0",
|
version: "0.1.0",
|
||||||
protocol_version: "1.0",
|
protocol_version: "1.1",
|
||||||
min_compatible_peer: "0.0.0",
|
min_compatible_peer: "0.0.0",
|
||||||
min_compatible_protocol: "1.0",
|
min_compatible_protocol: "1.1",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -102,7 +107,7 @@ describe("performHandshake", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(outcome.result.min_compatible_peer).toBe("0.0.0");
|
expect(outcome.result.min_compatible_peer).toBe("0.0.0");
|
||||||
expect(outcome.result.min_compatible_protocol).toBe("1.0");
|
expect(outcome.result.min_compatible_protocol).toBe("1.1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts legacy daemon reply with only min_compatible_peer", async () => {
|
it("accepts legacy daemon reply with only min_compatible_peer", async () => {
|
||||||
@@ -129,8 +134,8 @@ describe("performHandshake", () => {
|
|||||||
const response = {
|
const response = {
|
||||||
server: "browser-skill-daemon",
|
server: "browser-skill-daemon",
|
||||||
version: "0.1.0",
|
version: "0.1.0",
|
||||||
protocol_version: "1.0",
|
protocol_version: "1.1",
|
||||||
min_compatible_protocol: "1.0",
|
min_compatible_protocol: "1.1",
|
||||||
} satisfies HandshakeResult;
|
} satisfies HandshakeResult;
|
||||||
const transport = fakeTransport((req) => ({
|
const transport = fakeTransport((req) => ({
|
||||||
id: (req as { id: string }).id,
|
id: (req as { id: string }).id,
|
||||||
@@ -145,7 +150,7 @@ describe("performHandshake", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(outcome.result.min_compatible_peer).toBeUndefined();
|
expect(outcome.result.min_compatible_peer).toBeUndefined();
|
||||||
expect(outcome.result.min_compatible_protocol).toBe("1.0");
|
expect(outcome.result.min_compatible_protocol).toBe("1.1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects when the daemon responds with an error", async () => {
|
it("rejects when the daemon responds with an error", async () => {
|
||||||
@@ -178,9 +183,9 @@ describe("performHandshake", () => {
|
|||||||
result: {
|
result: {
|
||||||
server: "browser-skill-daemon",
|
server: "browser-skill-daemon",
|
||||||
version: "0.1.0",
|
version: "0.1.0",
|
||||||
protocol_version: "1.0",
|
protocol_version: "1.1",
|
||||||
min_compatible_peer: "0.0.0",
|
min_compatible_peer: "0.0.0",
|
||||||
min_compatible_protocol: "1.0",
|
min_compatible_protocol: "1.1",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
emit({
|
emit({
|
||||||
@@ -188,14 +193,14 @@ describe("performHandshake", () => {
|
|||||||
result: {
|
result: {
|
||||||
server: "browser-skill-daemon",
|
server: "browser-skill-daemon",
|
||||||
version: "0.1.0",
|
version: "0.1.0",
|
||||||
protocol_version: "1.0",
|
protocol_version: "1.1",
|
||||||
min_compatible_peer: "0.0.0",
|
min_compatible_peer: "0.0.0",
|
||||||
min_compatible_protocol: "1.0",
|
min_compatible_protocol: "1.1",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(pending).resolves.toMatchObject({
|
await expect(pending).resolves.toMatchObject({
|
||||||
result: { server: "browser-skill-daemon", protocol_version: "1.0" },
|
result: { server: "browser-skill-daemon", protocol_version: "1.1" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type {
|
|||||||
ResponseFrame,
|
ResponseFrame,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const PROTOCOL_VERSION = "1.0";
|
export const PROTOCOL_VERSION = "1.1";
|
||||||
/**
|
/**
|
||||||
* Extension semver, injected at build time from `package.json` via
|
* Extension semver, injected at build time from `package.json` via
|
||||||
* Vite's `define` (see `wxt.config.ts` and `vitest.config.ts`).
|
* Vite's `define` (see `wxt.config.ts` and `vitest.config.ts`).
|
||||||
@@ -18,7 +18,7 @@ export const EXTENSION_VERSION: string =
|
|||||||
* Lowest **protocol** version this extension accepts (e.g. `"1.0"`).
|
* Lowest **protocol** version this extension accepts (e.g. `"1.0"`).
|
||||||
* Must stay in sync with daemon `MIN_COMPATIBLE_PROTOCOL`.
|
* Must stay in sync with daemon `MIN_COMPATIBLE_PROTOCOL`.
|
||||||
*/
|
*/
|
||||||
export const MIN_COMPATIBLE_PROTOCOL = "1.0";
|
export const MIN_COMPATIBLE_PROTOCOL = "1.1";
|
||||||
/**
|
/**
|
||||||
* **Deprecated** — legacy app-semver floor for wire compat with old
|
* **Deprecated** — legacy app-semver floor for wire compat with old
|
||||||
* daemons. New code sends `"0.0.0"`; compat decisions ignore this.
|
* daemons. New code sends `"0.0.0"`; compat decisions ignore this.
|
||||||
|
|||||||
@@ -480,7 +480,7 @@ mod m2_tests {
|
|||||||
fn fake_status(browsers: Vec<BrowserStatusEntry>, skew: Vec<VersionSkewEntry>) -> StatusResult {
|
fn fake_status(browsers: Vec<BrowserStatusEntry>, skew: Vec<VersionSkewEntry>) -> StatusResult {
|
||||||
StatusResult {
|
StatusResult {
|
||||||
daemon_version: env!("CARGO_PKG_VERSION").into(),
|
daemon_version: env!("CARGO_PKG_VERSION").into(),
|
||||||
protocol_version: "1.0".into(),
|
protocol_version: "1.1".into(),
|
||||||
pid: 1,
|
pid: 1,
|
||||||
uptime_secs: 0,
|
uptime_secs: 0,
|
||||||
ws_port: 0,
|
ws_port: 0,
|
||||||
@@ -545,7 +545,7 @@ mod m2_tests {
|
|||||||
session_count: 0,
|
session_count: 0,
|
||||||
connected_at_ms: 1,
|
connected_at_ms: 1,
|
||||||
version_skew: false,
|
version_skew: false,
|
||||||
extension_protocol_version: "1.0".into(),
|
extension_protocol_version: "1.1".into(),
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
);
|
);
|
||||||
@@ -566,7 +566,7 @@ mod m2_tests {
|
|||||||
session_count: 0,
|
session_count: 0,
|
||||||
connected_at_ms: 1,
|
connected_at_ms: 1,
|
||||||
version_skew: true,
|
version_skew: true,
|
||||||
extension_protocol_version: "1.1".into(),
|
extension_protocol_version: "1.2".into(),
|
||||||
}],
|
}],
|
||||||
vec![VersionSkewEntry {
|
vec![VersionSkewEntry {
|
||||||
instance_id: "alpha".into(),
|
instance_id: "alpha".into(),
|
||||||
@@ -574,8 +574,8 @@ mod m2_tests {
|
|||||||
label: "Personal".into(),
|
label: "Personal".into(),
|
||||||
server_version: env!("CARGO_PKG_VERSION").into(),
|
server_version: env!("CARGO_PKG_VERSION").into(),
|
||||||
client_version: "0.0.9".into(),
|
client_version: "0.0.9".into(),
|
||||||
server_protocol_version: "1.0".into(),
|
server_protocol_version: "1.1".into(),
|
||||||
client_protocol_version: "1.1".into(),
|
client_protocol_version: "1.2".into(),
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
let check = check_browsers_protocol_compatible(Some(&status));
|
let check = check_browsers_protocol_compatible(Some(&status));
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ use crate::daemon::{
|
|||||||
browsers::{BROWSER_LIVENESS_TICK, BROWSER_LIVENESS_TIMEOUT, EXTENSION_CONNECT_WAIT},
|
browsers::{BROWSER_LIVENESS_TICK, BROWSER_LIVENESS_TIMEOUT, EXTENSION_CONNECT_WAIT},
|
||||||
info as daemon_info, ipc, lockfile, paths,
|
info as daemon_info, ipc, lockfile, paths,
|
||||||
sessions::{StopSessionError, forget_session, stop_session},
|
sessions::{StopSessionError, forget_session, stop_session},
|
||||||
state::DaemonState,
|
state::{DaemonState, PROTOCOL_VERSION},
|
||||||
ws,
|
ws,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -299,7 +299,7 @@ pub fn run_foreground(cfg: DaemonConfig) -> Result<()> {
|
|||||||
ws_port,
|
ws_port,
|
||||||
sock_path: sock_path.clone(),
|
sock_path: sock_path.clone(),
|
||||||
daemon_version: env!("CARGO_PKG_VERSION"),
|
daemon_version: env!("CARGO_PKG_VERSION"),
|
||||||
protocol_version: "1.0",
|
protocol_version: PROTOCOL_VERSION,
|
||||||
};
|
};
|
||||||
let handler = ipc::full_handler(status, Arc::clone(&state));
|
let handler = ipc::full_handler(status, Arc::clone(&state));
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ use super::start::DaemonConfig;
|
|||||||
use super::ws::WsHandle;
|
use super::ws::WsHandle;
|
||||||
|
|
||||||
pub const DAEMON_VERSION: &str = env!("CARGO_PKG_VERSION");
|
pub const DAEMON_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
pub const PROTOCOL_VERSION: &str = "1.0";
|
pub const PROTOCOL_VERSION: &str = "1.1";
|
||||||
/// Lowest **protocol** version peers must speak (e.g. `"1.0"`).
|
/// Lowest **protocol** version peers must speak (e.g. `"1.1"`).
|
||||||
pub const MIN_COMPATIBLE_PROTOCOL: &str = "1.0";
|
pub const MIN_COMPATIBLE_PROTOCOL: &str = "1.1";
|
||||||
/// Legacy app-semver floor used only when `HandshakeResult.min_compatible_peer`
|
/// Legacy app-semver floor used only when `HandshakeResult.min_compatible_peer`
|
||||||
/// is emitted for old extensions. New code ignores this on read.
|
/// is emitted for old extensions. New code ignores this on read.
|
||||||
pub const LEGACY_MIN_COMPATIBLE_PEER: &str = "0.0.0";
|
pub const LEGACY_MIN_COMPATIBLE_PEER: &str = "0.0.0";
|
||||||
|
|||||||
@@ -112,14 +112,14 @@ async fn handshake_as_ext(
|
|||||||
let params = HandshakeParams {
|
let params = HandshakeParams {
|
||||||
client: "browser-skill-extension".into(),
|
client: "browser-skill-extension".into(),
|
||||||
version: "0.1.0-dev.0".parse().unwrap(),
|
version: "0.1.0-dev.0".parse().unwrap(),
|
||||||
protocol_version: "1.0".into(),
|
protocol_version: "1.1".into(),
|
||||||
instance_id: TEST_EXT_ID.into(),
|
instance_id: TEST_EXT_ID.into(),
|
||||||
browser: BrowserPeerInfo {
|
browser: BrowserPeerInfo {
|
||||||
name: "chrome".into(),
|
name: "chrome".into(),
|
||||||
version: "131.0".into(),
|
version: "131.0".into(),
|
||||||
},
|
},
|
||||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||||
min_compatible_protocol: Some("1.0".into()),
|
min_compatible_protocol: Some("1.1".into()),
|
||||||
label: "Test".into(),
|
label: "Test".into(),
|
||||||
};
|
};
|
||||||
let req = RequestFrame {
|
let req = RequestFrame {
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ async fn handshake_as_ext(
|
|||||||
let params = HandshakeParams {
|
let params = HandshakeParams {
|
||||||
client: "browser-skill-extension".into(),
|
client: "browser-skill-extension".into(),
|
||||||
version: "0.1.0-dev.0".parse().unwrap(),
|
version: "0.1.0-dev.0".parse().unwrap(),
|
||||||
protocol_version: "1.0".into(),
|
protocol_version: "1.1".into(),
|
||||||
instance_id: TEST_EXT_ID.into(),
|
instance_id: TEST_EXT_ID.into(),
|
||||||
browser: BrowserPeerInfo {
|
browser: BrowserPeerInfo {
|
||||||
name: "chrome".into(),
|
name: "chrome".into(),
|
||||||
@@ -83,7 +83,7 @@ async fn handshake_as_ext(
|
|||||||
},
|
},
|
||||||
label: "Test".into(),
|
label: "Test".into(),
|
||||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||||
min_compatible_protocol: Some("1.0".into()),
|
min_compatible_protocol: Some("1.1".into()),
|
||||||
};
|
};
|
||||||
let req = RequestFrame {
|
let req = RequestFrame {
|
||||||
id: "hs".into(),
|
id: "hs".into(),
|
||||||
|
|||||||
@@ -108,12 +108,12 @@ async fn send_handshake_with_floors(
|
|||||||
async fn handshake_ok_when_protocol_matches() {
|
async fn handshake_ok_when_protocol_matches() {
|
||||||
let (handle, _sock) = spawn_daemon().await;
|
let (handle, _sock) = spawn_daemon().await;
|
||||||
let mut ws = open_ws(handle.ws_addr()).await;
|
let mut ws = open_ws(handle.ws_addr()).await;
|
||||||
let resp = send_handshake(&mut ws, "1.0", env!("CARGO_PKG_VERSION")).await;
|
let resp = send_handshake(&mut ws, "1.1", env!("CARGO_PKG_VERSION")).await;
|
||||||
let result: HandshakeResult = match resp.body {
|
let result: HandshakeResult = match resp.body {
|
||||||
ResponseBody::Ok(v) => serde_json::from_value(v).unwrap(),
|
ResponseBody::Ok(v) => serde_json::from_value(v).unwrap(),
|
||||||
ResponseBody::Err(e) => panic!("expected ok handshake, got {e:?}"),
|
ResponseBody::Err(e) => panic!("expected ok handshake, got {e:?}"),
|
||||||
};
|
};
|
||||||
assert_eq!(result.protocol_version, "1.0");
|
assert_eq!(result.protocol_version, "1.1");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result
|
result
|
||||||
.min_compatible_peer
|
.min_compatible_peer
|
||||||
@@ -124,7 +124,7 @@ async fn handshake_ok_when_protocol_matches() {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.min_compatible_protocol.as_deref(),
|
result.min_compatible_protocol.as_deref(),
|
||||||
Some("1.0"),
|
Some("1.1"),
|
||||||
"daemon must advertise protocol floor for new extensions"
|
"daemon must advertise protocol floor for new extensions"
|
||||||
);
|
);
|
||||||
handle.shutdown().await;
|
handle.shutdown().await;
|
||||||
@@ -135,7 +135,7 @@ async fn handshake_ok_when_app_versions_differ_but_protocol_matches() {
|
|||||||
let (handle, _sock) = spawn_daemon().await;
|
let (handle, _sock) = spawn_daemon().await;
|
||||||
let mut ws = open_ws(handle.ws_addr()).await;
|
let mut ws = open_ws(handle.ws_addr()).await;
|
||||||
let resp =
|
let resp =
|
||||||
send_handshake_with_floors(&mut ws, "1.0", "9.9.9", Some("0.0.0"), Some("1.0")).await;
|
send_handshake_with_floors(&mut ws, "1.1", "9.9.9", Some("0.0.0"), Some("1.1")).await;
|
||||||
match resp.body {
|
match resp.body {
|
||||||
ResponseBody::Ok(_) => {}
|
ResponseBody::Ok(_) => {}
|
||||||
other => panic!("expected ok when protocol matches, got {other:?}"),
|
other => panic!("expected ok when protocol matches, got {other:?}"),
|
||||||
@@ -149,10 +149,10 @@ async fn handshake_skew_when_protocol_minor_differs() {
|
|||||||
let mut ws = open_ws(handle.ws_addr()).await;
|
let mut ws = open_ws(handle.ws_addr()).await;
|
||||||
let resp = send_handshake_with_floors(
|
let resp = send_handshake_with_floors(
|
||||||
&mut ws,
|
&mut ws,
|
||||||
"1.1",
|
"1.2",
|
||||||
env!("CARGO_PKG_VERSION"),
|
env!("CARGO_PKG_VERSION"),
|
||||||
Some("0.0.0"),
|
Some("0.0.0"),
|
||||||
Some("1.0"),
|
Some("1.1"),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
match resp.body {
|
match resp.body {
|
||||||
@@ -168,6 +168,28 @@ async fn handshake_skew_when_protocol_minor_differs() {
|
|||||||
handle.shutdown().await;
|
handle.shutdown().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handshake_rejects_pre_trace_v3_extension() {
|
||||||
|
let (handle, _sock) = spawn_daemon().await;
|
||||||
|
let mut ws = open_ws(handle.ws_addr()).await;
|
||||||
|
let resp = send_handshake_with_floors(
|
||||||
|
&mut ws,
|
||||||
|
"1.0",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
Some("0.0.0"),
|
||||||
|
Some("1.0"),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
match resp.body {
|
||||||
|
ResponseBody::Err(e) => {
|
||||||
|
assert_eq!(e.code, ErrorCode::VersionTooOld);
|
||||||
|
assert!(e.message.contains("below local min_compatible_protocol"));
|
||||||
|
}
|
||||||
|
other => panic!("pre-Trace-v3 extension must be rejected, got {other:?}"),
|
||||||
|
}
|
||||||
|
handle.shutdown().await;
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn handshake_rejected_on_protocol_major_mismatch() {
|
async fn handshake_rejected_on_protocol_major_mismatch() {
|
||||||
let (handle, _sock) = spawn_daemon().await;
|
let (handle, _sock) = spawn_daemon().await;
|
||||||
@@ -210,7 +232,7 @@ async fn handshake_legacy_ext_without_protocol_floor_still_ok() {
|
|||||||
let mut ws = open_ws(handle.ws_addr()).await;
|
let mut ws = open_ws(handle.ws_addr()).await;
|
||||||
let resp = send_handshake_with_floors(
|
let resp = send_handshake_with_floors(
|
||||||
&mut ws,
|
&mut ws,
|
||||||
"1.0",
|
"1.1",
|
||||||
env!("CARGO_PKG_VERSION"),
|
env!("CARGO_PKG_VERSION"),
|
||||||
Some("0.1.0"),
|
Some("0.1.0"),
|
||||||
None,
|
None,
|
||||||
@@ -235,7 +257,7 @@ async fn status_surfaces_version_skew_for_skewed_browser() {
|
|||||||
browser_name: "chrome".into(),
|
browser_name: "chrome".into(),
|
||||||
browser_version: "131.0".into(),
|
browser_version: "131.0".into(),
|
||||||
extension_version: "9.9.9".into(),
|
extension_version: "9.9.9".into(),
|
||||||
extension_protocol_version: "1.1".into(),
|
extension_protocol_version: "1.2".into(),
|
||||||
label: "Older".into(),
|
label: "Older".into(),
|
||||||
sink: bsk::daemon::browsers::BrowserSink { tx },
|
sink: bsk::daemon::browsers::BrowserSink { tx },
|
||||||
pending: Mutex::new(bsk::daemon::browsers::Pending::default()),
|
pending: Mutex::new(bsk::daemon::browsers::Pending::default()),
|
||||||
@@ -263,8 +285,8 @@ async fn status_surfaces_version_skew_for_skewed_browser() {
|
|||||||
.iter()
|
.iter()
|
||||||
.find(|s| s.instance_id == "skew-only-test")
|
.find(|s| s.instance_id == "skew-only-test")
|
||||||
.expect("status must list our skew client");
|
.expect("status must list our skew client");
|
||||||
assert_eq!(skew.client_protocol_version, "1.1");
|
assert_eq!(skew.client_protocol_version, "1.2");
|
||||||
assert_eq!(skew.server_protocol_version, "1.0");
|
assert_eq!(skew.server_protocol_version, "1.1");
|
||||||
assert_eq!(skew.client_version, "9.9.9");
|
assert_eq!(skew.client_version, "9.9.9");
|
||||||
let entry = status
|
let entry = status
|
||||||
.browsers
|
.browsers
|
||||||
@@ -281,7 +303,7 @@ async fn handshake_rejects_when_local_below_peer_min_compatible_protocol() {
|
|||||||
let mut ws = open_ws(handle.ws_addr()).await;
|
let mut ws = open_ws(handle.ws_addr()).await;
|
||||||
let resp = send_handshake_with_floors(
|
let resp = send_handshake_with_floors(
|
||||||
&mut ws,
|
&mut ws,
|
||||||
"1.0",
|
"1.1",
|
||||||
env!("CARGO_PKG_VERSION"),
|
env!("CARGO_PKG_VERSION"),
|
||||||
Some("0.0.0"),
|
Some("0.0.0"),
|
||||||
Some("99.0.0"),
|
Some("99.0.0"),
|
||||||
|
|||||||
@@ -81,14 +81,14 @@ async fn handshake_as_ext(
|
|||||||
let params = HandshakeParams {
|
let params = HandshakeParams {
|
||||||
client: "browser-skill-extension".into(),
|
client: "browser-skill-extension".into(),
|
||||||
version: "0.1.0-dev.0".parse().unwrap(),
|
version: "0.1.0-dev.0".parse().unwrap(),
|
||||||
protocol_version: "1.0".into(),
|
protocol_version: "1.1".into(),
|
||||||
instance_id: TEST_EXT_ID.into(),
|
instance_id: TEST_EXT_ID.into(),
|
||||||
browser: BrowserPeerInfo {
|
browser: BrowserPeerInfo {
|
||||||
name: "chrome".into(),
|
name: "chrome".into(),
|
||||||
version: "131.0".into(),
|
version: "131.0".into(),
|
||||||
},
|
},
|
||||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||||
min_compatible_protocol: Some("1.0".into()),
|
min_compatible_protocol: Some("1.1".into()),
|
||||||
label: "Test".into(),
|
label: "Test".into(),
|
||||||
};
|
};
|
||||||
let req = RequestFrame {
|
let req = RequestFrame {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ async fn handshake_as_ext(
|
|||||||
let params = HandshakeParams {
|
let params = HandshakeParams {
|
||||||
client: "browser-skill-extension".into(),
|
client: "browser-skill-extension".into(),
|
||||||
version: "0.1.0-dev.0".parse().unwrap(),
|
version: "0.1.0-dev.0".parse().unwrap(),
|
||||||
protocol_version: "1.0".into(),
|
protocol_version: "1.1".into(),
|
||||||
instance_id: TEST_EXT_ID.into(),
|
instance_id: TEST_EXT_ID.into(),
|
||||||
browser: BrowserPeerInfo {
|
browser: BrowserPeerInfo {
|
||||||
name: "chrome".into(),
|
name: "chrome".into(),
|
||||||
@@ -89,7 +89,7 @@ async fn handshake_as_ext(
|
|||||||
},
|
},
|
||||||
label: "Test".into(),
|
label: "Test".into(),
|
||||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||||
min_compatible_protocol: Some("1.0".into()),
|
min_compatible_protocol: Some("1.1".into()),
|
||||||
};
|
};
|
||||||
let req = RequestFrame {
|
let req = RequestFrame {
|
||||||
id: "hs".into(),
|
id: "hs".into(),
|
||||||
|
|||||||
@@ -84,14 +84,14 @@ async fn handshake_as_ext(
|
|||||||
let params = HandshakeParams {
|
let params = HandshakeParams {
|
||||||
client: "browser-skill-extension".into(),
|
client: "browser-skill-extension".into(),
|
||||||
version: "0.1.0-dev.0".parse().unwrap(),
|
version: "0.1.0-dev.0".parse().unwrap(),
|
||||||
protocol_version: "1.0".into(),
|
protocol_version: "1.1".into(),
|
||||||
instance_id: TEST_EXT_ID.into(),
|
instance_id: TEST_EXT_ID.into(),
|
||||||
browser: BrowserPeerInfo {
|
browser: BrowserPeerInfo {
|
||||||
name: "chrome".into(),
|
name: "chrome".into(),
|
||||||
version: "131.0".into(),
|
version: "131.0".into(),
|
||||||
},
|
},
|
||||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||||
min_compatible_protocol: Some("1.0".into()),
|
min_compatible_protocol: Some("1.1".into()),
|
||||||
label: "Test".into(),
|
label: "Test".into(),
|
||||||
};
|
};
|
||||||
let req = RequestFrame {
|
let req = RequestFrame {
|
||||||
@@ -400,14 +400,14 @@ async fn connect_second_ext(
|
|||||||
let params = HandshakeParams {
|
let params = HandshakeParams {
|
||||||
client: "browser-skill-extension".into(),
|
client: "browser-skill-extension".into(),
|
||||||
version: "0.1.0-dev.0".parse().unwrap(),
|
version: "0.1.0-dev.0".parse().unwrap(),
|
||||||
protocol_version: "1.0".into(),
|
protocol_version: "1.1".into(),
|
||||||
instance_id: instance_id.into(),
|
instance_id: instance_id.into(),
|
||||||
browser: BrowserPeerInfo {
|
browser: BrowserPeerInfo {
|
||||||
name: "edge".into(),
|
name: "edge".into(),
|
||||||
version: "130".into(),
|
version: "130".into(),
|
||||||
},
|
},
|
||||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||||
min_compatible_protocol: Some("1.0".into()),
|
min_compatible_protocol: Some("1.1".into()),
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
};
|
};
|
||||||
let hs = RequestFrame {
|
let hs = RequestFrame {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ fn bsk_status_json_returns_structured_payload() {
|
|||||||
|
|
||||||
assert!(parsed["pid"].as_u64().unwrap() > 0);
|
assert!(parsed["pid"].as_u64().unwrap() > 0);
|
||||||
assert!(!parsed["daemon_version"].as_str().unwrap().is_empty());
|
assert!(!parsed["daemon_version"].as_str().unwrap().is_empty());
|
||||||
assert_eq!(parsed["protocol_version"], "1.0");
|
assert_eq!(parsed["protocol_version"], "1.1");
|
||||||
assert!(parsed["sock_path"].as_str().is_some());
|
assert!(parsed["sock_path"].as_str().is_some());
|
||||||
assert_eq!(parsed["browsers"], serde_json::json!([]));
|
assert_eq!(parsed["browsers"], serde_json::json!([]));
|
||||||
assert_eq!(parsed["sessions"], serde_json::json!([]));
|
assert_eq!(parsed["sessions"], serde_json::json!([]));
|
||||||
|
|||||||
@@ -73,14 +73,14 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult {
|
|||||||
let params = HandshakeParams {
|
let params = HandshakeParams {
|
||||||
client: "browser-skill-extension".into(),
|
client: "browser-skill-extension".into(),
|
||||||
version: "0.1.0-dev.0".parse().unwrap(),
|
version: "0.1.0-dev.0".parse().unwrap(),
|
||||||
protocol_version: "1.0".into(),
|
protocol_version: "1.1".into(),
|
||||||
instance_id: TEST_EXT_ID.into(),
|
instance_id: TEST_EXT_ID.into(),
|
||||||
browser: BrowserPeerInfo {
|
browser: BrowserPeerInfo {
|
||||||
name: "chrome".into(),
|
name: "chrome".into(),
|
||||||
version: "131.0".into(),
|
version: "131.0".into(),
|
||||||
},
|
},
|
||||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||||
min_compatible_protocol: Some("1.0".into()),
|
min_compatible_protocol: Some("1.1".into()),
|
||||||
label: "Test".into(),
|
label: "Test".into(),
|
||||||
};
|
};
|
||||||
let req = RequestFrame {
|
let req = RequestFrame {
|
||||||
|
|||||||
@@ -76,14 +76,14 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult {
|
|||||||
let params = HandshakeParams {
|
let params = HandshakeParams {
|
||||||
client: "browser-skill-extension".into(),
|
client: "browser-skill-extension".into(),
|
||||||
version: "0.1.0-dev.0".parse().unwrap(),
|
version: "0.1.0-dev.0".parse().unwrap(),
|
||||||
protocol_version: "1.0".into(),
|
protocol_version: "1.1".into(),
|
||||||
instance_id: TEST_EXT_ID.into(),
|
instance_id: TEST_EXT_ID.into(),
|
||||||
browser: BrowserPeerInfo {
|
browser: BrowserPeerInfo {
|
||||||
name: "chrome".into(),
|
name: "chrome".into(),
|
||||||
version: "131.0".into(),
|
version: "131.0".into(),
|
||||||
},
|
},
|
||||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||||
min_compatible_protocol: Some("1.0".into()),
|
min_compatible_protocol: Some("1.1".into()),
|
||||||
label: "Test".into(),
|
label: "Test".into(),
|
||||||
};
|
};
|
||||||
let req = RequestFrame {
|
let req = RequestFrame {
|
||||||
|
|||||||
@@ -75,14 +75,14 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult {
|
|||||||
let params = HandshakeParams {
|
let params = HandshakeParams {
|
||||||
client: "browser-skill-extension".into(),
|
client: "browser-skill-extension".into(),
|
||||||
version: "0.1.0-dev.0".parse().unwrap(),
|
version: "0.1.0-dev.0".parse().unwrap(),
|
||||||
protocol_version: "1.0".into(),
|
protocol_version: "1.1".into(),
|
||||||
instance_id: TEST_EXT_ID.into(),
|
instance_id: TEST_EXT_ID.into(),
|
||||||
browser: BrowserPeerInfo {
|
browser: BrowserPeerInfo {
|
||||||
name: "chrome".into(),
|
name: "chrome".into(),
|
||||||
version: "131.0".into(),
|
version: "131.0".into(),
|
||||||
},
|
},
|
||||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||||
min_compatible_protocol: Some("1.0".into()),
|
min_compatible_protocol: Some("1.1".into()),
|
||||||
label: "Test".into(),
|
label: "Test".into(),
|
||||||
};
|
};
|
||||||
let req = RequestFrame {
|
let req = RequestFrame {
|
||||||
|
|||||||
@@ -85,14 +85,14 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult {
|
|||||||
let params = HandshakeParams {
|
let params = HandshakeParams {
|
||||||
client: "browser-skill-extension".into(),
|
client: "browser-skill-extension".into(),
|
||||||
version: "0.1.0-dev.0".parse().unwrap(),
|
version: "0.1.0-dev.0".parse().unwrap(),
|
||||||
protocol_version: "1.0".into(),
|
protocol_version: "1.1".into(),
|
||||||
instance_id: TEST_EXT_ID.into(),
|
instance_id: TEST_EXT_ID.into(),
|
||||||
browser: BrowserPeerInfo {
|
browser: BrowserPeerInfo {
|
||||||
name: "chrome".into(),
|
name: "chrome".into(),
|
||||||
version: "131.0".into(),
|
version: "131.0".into(),
|
||||||
},
|
},
|
||||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||||
min_compatible_protocol: Some("1.0".into()),
|
min_compatible_protocol: Some("1.1".into()),
|
||||||
label: "Test".into(),
|
label: "Test".into(),
|
||||||
};
|
};
|
||||||
let req = RequestFrame {
|
let req = RequestFrame {
|
||||||
|
|||||||
@@ -53,14 +53,14 @@ pub async fn send_handshake(
|
|||||||
let params = HandshakeParams {
|
let params = HandshakeParams {
|
||||||
client: "browser-skill-extension".into(),
|
client: "browser-skill-extension".into(),
|
||||||
version: "0.1.0-dev.0".parse().unwrap(),
|
version: "0.1.0-dev.0".parse().unwrap(),
|
||||||
protocol_version: "1.0".into(),
|
protocol_version: "1.1".into(),
|
||||||
instance_id: instance_id.into(),
|
instance_id: instance_id.into(),
|
||||||
browser: BrowserPeerInfo {
|
browser: BrowserPeerInfo {
|
||||||
name: "chrome".into(),
|
name: "chrome".into(),
|
||||||
version: "131.0".into(),
|
version: "131.0".into(),
|
||||||
},
|
},
|
||||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||||
min_compatible_protocol: Some("1.0".into()),
|
min_compatible_protocol: Some("1.1".into()),
|
||||||
label: "Test Chrome".into(),
|
label: "Test Chrome".into(),
|
||||||
};
|
};
|
||||||
let req = RequestFrame {
|
let req = RequestFrame {
|
||||||
@@ -91,7 +91,7 @@ async fn ws_handshake_registers_browser_in_state() {
|
|||||||
let mut ws = connect_ext(handle.ws_addr(), &origin).await;
|
let mut ws = connect_ext(handle.ws_addr(), &origin).await;
|
||||||
let result = send_handshake(&mut ws, TEST_EXT_ID).await;
|
let result = send_handshake(&mut ws, TEST_EXT_ID).await;
|
||||||
assert_eq!(result.server, "browser-skill-daemon");
|
assert_eq!(result.server, "browser-skill-daemon");
|
||||||
assert_eq!(result.protocol_version, "1.0");
|
assert_eq!(result.protocol_version, "1.1");
|
||||||
|
|
||||||
let state = handle.state();
|
let state = handle.state();
|
||||||
let browsers = state.browsers.snapshot();
|
let browsers = state.browsers.snapshot();
|
||||||
|
|||||||
@@ -11,6 +11,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
"FillCommit": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"enter",
|
||||||
|
"suggestion",
|
||||||
|
"blur"
|
||||||
|
]
|
||||||
|
},
|
||||||
"KeyModifier": {
|
"KeyModifier": {
|
||||||
"description": "Keyboard modifier flags. Multiple flags may be combined; the extension folds them into CDP's bitfield (`alt=1, ctrl=2, meta=4, shift=8`).",
|
"description": "Keyboard modifier flags. Multiple flags may be combined; the extension folds them into CDP's bitfield (`alt=1, ctrl=2, meta=4, shift=8`).",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -21,25 +29,32 @@
|
|||||||
"shift"
|
"shift"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"PageRef": {
|
"NavigationCause": {
|
||||||
"description": "Page context dictionary entry — referenced by steps via `page` id.",
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"user_typed",
|
||||||
|
"link",
|
||||||
|
"form_submit",
|
||||||
|
"reload",
|
||||||
|
"history",
|
||||||
|
"script",
|
||||||
|
"browser"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"RecorderInfo": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"bsk",
|
||||||
"url"
|
"vom"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"bsk": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"title": {
|
"vom": {
|
||||||
"type": [
|
"type": "integer",
|
||||||
"string",
|
"format": "uint32",
|
||||||
"null"
|
"minimum": 0.0
|
||||||
]
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -68,21 +83,16 @@
|
|||||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
"cause",
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
|
"state",
|
||||||
"to"
|
"to"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
"cause": {
|
||||||
"anyOf": [
|
"$ref": "#/definitions/NavigationCause"
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -95,8 +105,11 @@
|
|||||||
"navigate"
|
"navigate"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"to": {
|
"to": {
|
||||||
@@ -110,20 +123,11 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
|
"state",
|
||||||
"target"
|
"target"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -135,8 +139,11 @@
|
|||||||
"click"
|
"click"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
@@ -150,20 +157,11 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
|
"state",
|
||||||
"target"
|
"target"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -175,8 +173,11 @@
|
|||||||
"hover"
|
"hover"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
@@ -188,22 +189,17 @@
|
|||||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
"commit",
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
|
"state",
|
||||||
"target",
|
"target",
|
||||||
"value"
|
"value"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
"commit": {
|
||||||
"anyOf": [
|
"$ref": "#/definitions/FillCommit"
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -216,15 +212,15 @@
|
|||||||
"fill"
|
"fill"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
|
||||||
"description": "Reference into `pages[]`.",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"redacted": {
|
"redacted": {
|
||||||
"type": [
|
"type": "boolean"
|
||||||
"boolean",
|
},
|
||||||
"null"
|
"result": {
|
||||||
]
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
|
"type": "string"
|
||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
"$ref": "#/definitions/TargetDescriptor"
|
"$ref": "#/definitions/TargetDescriptor"
|
||||||
@@ -240,21 +236,11 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
"selection",
|
"state",
|
||||||
"target"
|
"target"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -266,9 +252,8 @@
|
|||||||
"select"
|
"select"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
"type": "string"
|
|
||||||
},
|
},
|
||||||
"selection": {
|
"selection": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@@ -276,6 +261,10 @@
|
|||||||
"$ref": "#/definitions/SelectedOption"
|
"$ref": "#/definitions/SelectedOption"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"target": {
|
"target": {
|
||||||
"$ref": "#/definitions/TargetDescriptor"
|
"$ref": "#/definitions/TargetDescriptor"
|
||||||
}
|
}
|
||||||
@@ -288,19 +277,10 @@
|
|||||||
"id",
|
"id",
|
||||||
"key",
|
"key",
|
||||||
"op",
|
"op",
|
||||||
"page"
|
"result",
|
||||||
|
"state"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -324,8 +304,11 @@
|
|||||||
"press"
|
"press"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
@@ -339,48 +322,74 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"op",
|
||||||
|
"result",
|
||||||
|
"state"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "uint32",
|
||||||
|
"minimum": 0.0
|
||||||
|
},
|
||||||
|
"op": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"scroll"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"StepEffect": {
|
"StepResult": {
|
||||||
"description": "Observed navigation after a step (objective fact only).",
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"navigated_to"
|
"state"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"navigated_to": {
|
"state": {
|
||||||
"description": "Reference into `pages[]` for the destination page.",
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"StopReason": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"user_finish",
|
||||||
|
"cli_stop"
|
||||||
|
]
|
||||||
|
},
|
||||||
"TargetDescriptor": {
|
"TargetDescriptor": {
|
||||||
"description": "Stable semantic handle for an interacted element.\n\n`name` and `nearby_label` are **untrusted page text**.",
|
"description": "Stable semantic handle for an interacted element within a page observation.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
|
||||||
"tag"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"ctx": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"name_attr": {
|
"ref": {
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nearby_label": {
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"placeholder": {
|
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
@@ -392,8 +401,8 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"tag": {
|
"unmatched": {
|
||||||
"type": "string"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -402,42 +411,54 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"entry",
|
"entry",
|
||||||
"pages",
|
|
||||||
"recorded_at",
|
"recorded_at",
|
||||||
"steps"
|
"recorder",
|
||||||
|
"states",
|
||||||
|
"steps",
|
||||||
|
"stopped_by",
|
||||||
|
"version"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"entry": {
|
"entry": {
|
||||||
"$ref": "#/definitions/TraceEntry"
|
"$ref": "#/definitions/TraceEntry"
|
||||||
},
|
},
|
||||||
"pages": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/PageRef"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"purpose": {
|
"purpose": {
|
||||||
"description": "Optional user-provided goal from `--purpose` (metadata only).",
|
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"recorded_at": {
|
"recorded_at": {
|
||||||
"description": "RFC 3339 timestamp when recording stopped.",
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"recorder": {
|
||||||
|
"$ref": "#/definitions/RecorderInfo"
|
||||||
|
},
|
||||||
"started_at": {
|
"started_at": {
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"states": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/TraceState"
|
||||||
|
}
|
||||||
|
},
|
||||||
"steps": {
|
"steps": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/Step"
|
"$ref": "#/definitions/Step"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"stopped_by": {
|
||||||
|
"$ref": "#/definitions/StopReason"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "uint32",
|
||||||
|
"minimum": 0.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -452,6 +473,45 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"TraceState": {
|
||||||
|
"description": "Page observation dictionary entry — referenced by steps via `state` / `result.state`.",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"body": {
|
||||||
|
"description": "Wire-only: full page observation (front matter + VOM body + annotations).",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"description": "Disk-only: filename under the bundle `pages/` directory.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"truncated": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,26 @@
|
|||||||
"session_id"
|
"session_id"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"max_page_tokens": {
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "uint32",
|
||||||
|
"minimum": 0.0
|
||||||
|
},
|
||||||
"purpose": {
|
"purpose": {
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"redact_values": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"session_id": {
|
"session_id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,6 +11,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
"FillCommit": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"enter",
|
||||||
|
"suggestion",
|
||||||
|
"blur"
|
||||||
|
]
|
||||||
|
},
|
||||||
"KeyModifier": {
|
"KeyModifier": {
|
||||||
"description": "Keyboard modifier flags. Multiple flags may be combined; the extension folds them into CDP's bitfield (`alt=1, ctrl=2, meta=4, shift=8`).",
|
"description": "Keyboard modifier flags. Multiple flags may be combined; the extension folds them into CDP's bitfield (`alt=1, ctrl=2, meta=4, shift=8`).",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -21,25 +29,32 @@
|
|||||||
"shift"
|
"shift"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"PageRef": {
|
"NavigationCause": {
|
||||||
"description": "Page context dictionary entry — referenced by steps via `page` id.",
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"user_typed",
|
||||||
|
"link",
|
||||||
|
"form_submit",
|
||||||
|
"reload",
|
||||||
|
"history",
|
||||||
|
"script",
|
||||||
|
"browser"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"RecorderInfo": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"bsk",
|
||||||
"url"
|
"vom"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"bsk": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"title": {
|
"vom": {
|
||||||
"type": [
|
"type": "integer",
|
||||||
"string",
|
"format": "uint32",
|
||||||
"null"
|
"minimum": 0.0
|
||||||
]
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -68,21 +83,16 @@
|
|||||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
"cause",
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
|
"state",
|
||||||
"to"
|
"to"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
"cause": {
|
||||||
"anyOf": [
|
"$ref": "#/definitions/NavigationCause"
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -95,8 +105,11 @@
|
|||||||
"navigate"
|
"navigate"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"to": {
|
"to": {
|
||||||
@@ -110,20 +123,11 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
|
"state",
|
||||||
"target"
|
"target"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -135,8 +139,11 @@
|
|||||||
"click"
|
"click"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
@@ -150,20 +157,11 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
|
"state",
|
||||||
"target"
|
"target"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -175,8 +173,11 @@
|
|||||||
"hover"
|
"hover"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
@@ -188,22 +189,17 @@
|
|||||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
"commit",
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
|
"state",
|
||||||
"target",
|
"target",
|
||||||
"value"
|
"value"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
"commit": {
|
||||||
"anyOf": [
|
"$ref": "#/definitions/FillCommit"
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -216,15 +212,15 @@
|
|||||||
"fill"
|
"fill"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
|
||||||
"description": "Reference into `pages[]`.",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"redacted": {
|
"redacted": {
|
||||||
"type": [
|
"type": "boolean"
|
||||||
"boolean",
|
},
|
||||||
"null"
|
"result": {
|
||||||
]
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
|
"type": "string"
|
||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
"$ref": "#/definitions/TargetDescriptor"
|
"$ref": "#/definitions/TargetDescriptor"
|
||||||
@@ -240,21 +236,11 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
"selection",
|
"state",
|
||||||
"target"
|
"target"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -266,9 +252,8 @@
|
|||||||
"select"
|
"select"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
"type": "string"
|
|
||||||
},
|
},
|
||||||
"selection": {
|
"selection": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@@ -276,6 +261,10 @@
|
|||||||
"$ref": "#/definitions/SelectedOption"
|
"$ref": "#/definitions/SelectedOption"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"target": {
|
"target": {
|
||||||
"$ref": "#/definitions/TargetDescriptor"
|
"$ref": "#/definitions/TargetDescriptor"
|
||||||
}
|
}
|
||||||
@@ -288,19 +277,10 @@
|
|||||||
"id",
|
"id",
|
||||||
"key",
|
"key",
|
||||||
"op",
|
"op",
|
||||||
"page"
|
"result",
|
||||||
|
"state"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -324,8 +304,11 @@
|
|||||||
"press"
|
"press"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
@@ -339,48 +322,74 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"op",
|
||||||
|
"result",
|
||||||
|
"state"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "uint32",
|
||||||
|
"minimum": 0.0
|
||||||
|
},
|
||||||
|
"op": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"scroll"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"StepEffect": {
|
"StepResult": {
|
||||||
"description": "Observed navigation after a step (objective fact only).",
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"navigated_to"
|
"state"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"navigated_to": {
|
"state": {
|
||||||
"description": "Reference into `pages[]` for the destination page.",
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"StopReason": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"user_finish",
|
||||||
|
"cli_stop"
|
||||||
|
]
|
||||||
|
},
|
||||||
"TargetDescriptor": {
|
"TargetDescriptor": {
|
||||||
"description": "Stable semantic handle for an interacted element.\n\n`name` and `nearby_label` are **untrusted page text**.",
|
"description": "Stable semantic handle for an interacted element within a page observation.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
|
||||||
"tag"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"ctx": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"name_attr": {
|
"ref": {
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nearby_label": {
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"placeholder": {
|
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
@@ -392,8 +401,8 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"tag": {
|
"unmatched": {
|
||||||
"type": "string"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -402,42 +411,54 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"entry",
|
"entry",
|
||||||
"pages",
|
|
||||||
"recorded_at",
|
"recorded_at",
|
||||||
"steps"
|
"recorder",
|
||||||
|
"states",
|
||||||
|
"steps",
|
||||||
|
"stopped_by",
|
||||||
|
"version"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"entry": {
|
"entry": {
|
||||||
"$ref": "#/definitions/TraceEntry"
|
"$ref": "#/definitions/TraceEntry"
|
||||||
},
|
},
|
||||||
"pages": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/PageRef"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"purpose": {
|
"purpose": {
|
||||||
"description": "Optional user-provided goal from `--purpose` (metadata only).",
|
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"recorded_at": {
|
"recorded_at": {
|
||||||
"description": "RFC 3339 timestamp when recording stopped.",
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"recorder": {
|
||||||
|
"$ref": "#/definitions/RecorderInfo"
|
||||||
|
},
|
||||||
"started_at": {
|
"started_at": {
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"states": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/TraceState"
|
||||||
|
}
|
||||||
|
},
|
||||||
"steps": {
|
"steps": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/Step"
|
"$ref": "#/definitions/Step"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"stopped_by": {
|
||||||
|
"$ref": "#/definitions/StopReason"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "uint32",
|
||||||
|
"minimum": 0.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -452,6 +473,45 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"TraceState": {
|
||||||
|
"description": "Page observation dictionary entry — referenced by steps via `state` / `result.state`.",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"body": {
|
||||||
|
"description": "Wire-only: full page observation (front matter + VOM body + annotations).",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"description": "Disk-only: filename under the bundle `pages/` directory.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"truncated": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,45 +5,65 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"entry",
|
"entry",
|
||||||
"pages",
|
|
||||||
"recorded_at",
|
"recorded_at",
|
||||||
"steps"
|
"recorder",
|
||||||
|
"states",
|
||||||
|
"steps",
|
||||||
|
"stopped_by",
|
||||||
|
"version"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"entry": {
|
"entry": {
|
||||||
"$ref": "#/definitions/TraceEntry"
|
"$ref": "#/definitions/TraceEntry"
|
||||||
},
|
},
|
||||||
"pages": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/PageRef"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"purpose": {
|
"purpose": {
|
||||||
"description": "Optional user-provided goal from `--purpose` (metadata only).",
|
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"recorded_at": {
|
"recorded_at": {
|
||||||
"description": "RFC 3339 timestamp when recording stopped.",
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"recorder": {
|
||||||
|
"$ref": "#/definitions/RecorderInfo"
|
||||||
|
},
|
||||||
"started_at": {
|
"started_at": {
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"states": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/TraceState"
|
||||||
|
}
|
||||||
|
},
|
||||||
"steps": {
|
"steps": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/Step"
|
"$ref": "#/definitions/Step"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"stopped_by": {
|
||||||
|
"$ref": "#/definitions/StopReason"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "uint32",
|
||||||
|
"minimum": 0.0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
"FillCommit": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"enter",
|
||||||
|
"suggestion",
|
||||||
|
"blur"
|
||||||
|
]
|
||||||
|
},
|
||||||
"KeyModifier": {
|
"KeyModifier": {
|
||||||
"description": "Keyboard modifier flags. Multiple flags may be combined; the extension folds them into CDP's bitfield (`alt=1, ctrl=2, meta=4, shift=8`).",
|
"description": "Keyboard modifier flags. Multiple flags may be combined; the extension folds them into CDP's bitfield (`alt=1, ctrl=2, meta=4, shift=8`).",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -54,25 +74,32 @@
|
|||||||
"shift"
|
"shift"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"PageRef": {
|
"NavigationCause": {
|
||||||
"description": "Page context dictionary entry — referenced by steps via `page` id.",
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"user_typed",
|
||||||
|
"link",
|
||||||
|
"form_submit",
|
||||||
|
"reload",
|
||||||
|
"history",
|
||||||
|
"script",
|
||||||
|
"browser"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"RecorderInfo": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"bsk",
|
||||||
"url"
|
"vom"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"bsk": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"title": {
|
"vom": {
|
||||||
"type": [
|
"type": "integer",
|
||||||
"string",
|
"format": "uint32",
|
||||||
"null"
|
"minimum": 0.0
|
||||||
]
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -101,21 +128,16 @@
|
|||||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
"cause",
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
|
"state",
|
||||||
"to"
|
"to"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
"cause": {
|
||||||
"anyOf": [
|
"$ref": "#/definitions/NavigationCause"
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -128,8 +150,11 @@
|
|||||||
"navigate"
|
"navigate"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"to": {
|
"to": {
|
||||||
@@ -143,20 +168,11 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
|
"state",
|
||||||
"target"
|
"target"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -168,8 +184,11 @@
|
|||||||
"click"
|
"click"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
@@ -183,20 +202,11 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
|
"state",
|
||||||
"target"
|
"target"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -208,8 +218,11 @@
|
|||||||
"hover"
|
"hover"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
@@ -221,22 +234,17 @@
|
|||||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
"commit",
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
|
"state",
|
||||||
"target",
|
"target",
|
||||||
"value"
|
"value"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
"commit": {
|
||||||
"anyOf": [
|
"$ref": "#/definitions/FillCommit"
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -249,15 +257,15 @@
|
|||||||
"fill"
|
"fill"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
|
||||||
"description": "Reference into `pages[]`.",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"redacted": {
|
"redacted": {
|
||||||
"type": [
|
"type": "boolean"
|
||||||
"boolean",
|
},
|
||||||
"null"
|
"result": {
|
||||||
]
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
|
"type": "string"
|
||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
"$ref": "#/definitions/TargetDescriptor"
|
"$ref": "#/definitions/TargetDescriptor"
|
||||||
@@ -273,21 +281,11 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
"selection",
|
"state",
|
||||||
"target"
|
"target"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -299,9 +297,8 @@
|
|||||||
"select"
|
"select"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
"type": "string"
|
|
||||||
},
|
},
|
||||||
"selection": {
|
"selection": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@@ -309,6 +306,10 @@
|
|||||||
"$ref": "#/definitions/SelectedOption"
|
"$ref": "#/definitions/SelectedOption"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"target": {
|
"target": {
|
||||||
"$ref": "#/definitions/TargetDescriptor"
|
"$ref": "#/definitions/TargetDescriptor"
|
||||||
}
|
}
|
||||||
@@ -321,19 +322,10 @@
|
|||||||
"id",
|
"id",
|
||||||
"key",
|
"key",
|
||||||
"op",
|
"op",
|
||||||
"page"
|
"result",
|
||||||
|
"state"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -357,8 +349,11 @@
|
|||||||
"press"
|
"press"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
@@ -372,48 +367,74 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"op",
|
||||||
|
"result",
|
||||||
|
"state"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "uint32",
|
||||||
|
"minimum": 0.0
|
||||||
|
},
|
||||||
|
"op": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"scroll"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"StepEffect": {
|
"StepResult": {
|
||||||
"description": "Observed navigation after a step (objective fact only).",
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"navigated_to"
|
"state"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"navigated_to": {
|
"state": {
|
||||||
"description": "Reference into `pages[]` for the destination page.",
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"StopReason": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"user_finish",
|
||||||
|
"cli_stop"
|
||||||
|
]
|
||||||
|
},
|
||||||
"TargetDescriptor": {
|
"TargetDescriptor": {
|
||||||
"description": "Stable semantic handle for an interacted element.\n\n`name` and `nearby_label` are **untrusted page text**.",
|
"description": "Stable semantic handle for an interacted element within a page observation.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
|
||||||
"tag"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"ctx": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"name_attr": {
|
"ref": {
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nearby_label": {
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"placeholder": {
|
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
@@ -425,8 +446,8 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"tag": {
|
"unmatched": {
|
||||||
"type": "string"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -441,6 +462,45 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"TraceState": {
|
||||||
|
"description": "Page observation dictionary entry — referenced by steps via `state` / `result.state`.",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"body": {
|
||||||
|
"description": "Wire-only: full page observation (front matter + VOM body + annotations).",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"description": "Disk-only: filename under the bundle `pages/` directory.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"truncated": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,21 +7,16 @@
|
|||||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
"cause",
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
|
"state",
|
||||||
"to"
|
"to"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
"cause": {
|
||||||
"anyOf": [
|
"$ref": "#/definitions/NavigationCause"
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -34,8 +29,11 @@
|
|||||||
"navigate"
|
"navigate"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"to": {
|
"to": {
|
||||||
@@ -49,20 +47,11 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
|
"state",
|
||||||
"target"
|
"target"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -74,8 +63,11 @@
|
|||||||
"click"
|
"click"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
@@ -89,20 +81,11 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
|
"state",
|
||||||
"target"
|
"target"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -114,8 +97,11 @@
|
|||||||
"hover"
|
"hover"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
@@ -127,22 +113,17 @@
|
|||||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
"commit",
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
|
"state",
|
||||||
"target",
|
"target",
|
||||||
"value"
|
"value"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
"commit": {
|
||||||
"anyOf": [
|
"$ref": "#/definitions/FillCommit"
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -155,15 +136,15 @@
|
|||||||
"fill"
|
"fill"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
|
||||||
"description": "Reference into `pages[]`.",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"redacted": {
|
"redacted": {
|
||||||
"type": [
|
"type": "boolean"
|
||||||
"boolean",
|
},
|
||||||
"null"
|
"result": {
|
||||||
]
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
|
"type": "string"
|
||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
"$ref": "#/definitions/TargetDescriptor"
|
"$ref": "#/definitions/TargetDescriptor"
|
||||||
@@ -179,21 +160,11 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"op",
|
"op",
|
||||||
"page",
|
"result",
|
||||||
"selection",
|
"state",
|
||||||
"target"
|
"target"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -205,9 +176,8 @@
|
|||||||
"select"
|
"select"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
"type": "string"
|
|
||||||
},
|
},
|
||||||
"selection": {
|
"selection": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@@ -215,6 +185,10 @@
|
|||||||
"$ref": "#/definitions/SelectedOption"
|
"$ref": "#/definitions/SelectedOption"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"target": {
|
"target": {
|
||||||
"$ref": "#/definitions/TargetDescriptor"
|
"$ref": "#/definitions/TargetDescriptor"
|
||||||
}
|
}
|
||||||
@@ -227,19 +201,10 @@
|
|||||||
"id",
|
"id",
|
||||||
"key",
|
"key",
|
||||||
"op",
|
"op",
|
||||||
"page"
|
"result",
|
||||||
|
"state"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"effect": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/StepEffect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -263,8 +228,11 @@
|
|||||||
"press"
|
"press"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"page": {
|
"result": {
|
||||||
"description": "Reference into `pages[]`.",
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
@@ -278,9 +246,47 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"op",
|
||||||
|
"result",
|
||||||
|
"state"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "uint32",
|
||||||
|
"minimum": 0.0
|
||||||
|
},
|
||||||
|
"op": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"scroll"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"$ref": "#/definitions/StepResult"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"description": "Observation id immediately before this action.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
"FillCommit": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"enter",
|
||||||
|
"suggestion",
|
||||||
|
"blur"
|
||||||
|
]
|
||||||
|
},
|
||||||
"KeyModifier": {
|
"KeyModifier": {
|
||||||
"description": "Keyboard modifier flags. Multiple flags may be combined; the extension folds them into CDP's bitfield (`alt=1, ctrl=2, meta=4, shift=8`).",
|
"description": "Keyboard modifier flags. Multiple flags may be combined; the extension folds them into CDP's bitfield (`alt=1, ctrl=2, meta=4, shift=8`).",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -291,6 +297,18 @@
|
|||||||
"shift"
|
"shift"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"NavigationCause": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"user_typed",
|
||||||
|
"link",
|
||||||
|
"form_submit",
|
||||||
|
"reload",
|
||||||
|
"history",
|
||||||
|
"script",
|
||||||
|
"browser"
|
||||||
|
]
|
||||||
|
},
|
||||||
"SelectedOption": {
|
"SelectedOption": {
|
||||||
"description": "One selected option (`select` op).",
|
"description": "One selected option (`select` op).",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -309,45 +327,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"StepEffect": {
|
"StepResult": {
|
||||||
"description": "Observed navigation after a step (objective fact only).",
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"navigated_to"
|
"state"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"navigated_to": {
|
"state": {
|
||||||
"description": "Reference into `pages[]` for the destination page.",
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"TargetDescriptor": {
|
"TargetDescriptor": {
|
||||||
"description": "Stable semantic handle for an interacted element.\n\n`name` and `nearby_label` are **untrusted page text**.",
|
"description": "Stable semantic handle for an interacted element within a page observation.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
|
||||||
"tag"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"ctx": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"name_attr": {
|
"ref": {
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nearby_label": {
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"placeholder": {
|
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
@@ -359,8 +366,8 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"tag": {
|
"unmatched": {
|
||||||
"type": "string"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,33 @@
|
|||||||
//! Semantic user-action recording (`tool.record_start` / `stop` / `await`).
|
//! Semantic user-action recording (`tool.record_start` / `stop` / `await`).
|
||||||
//!
|
//!
|
||||||
//! Trace v2 is a **record-only** log of user actions: what was clicked, filled,
|
//! Trace v3 is a **state-action-state** chain: each step binds to page
|
||||||
//! selected, and where navigation occurred. No LLM runs during recording, so
|
//! observations (VOM) captured before and after the action.
|
||||||
//! variable-vs-constant classification is **not** stored — executing agents
|
|
||||||
//! infer that at run time from raw values and control names.
|
|
||||||
|
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::interaction::KeyModifier;
|
use super::interaction::KeyModifier;
|
||||||
|
|
||||||
|
pub const TRACE_VERSION: u32 = 3;
|
||||||
|
pub const VOM_FORMAT_VERSION: u32 = 1;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Target
|
// Target
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Stable semantic handle for an interacted element.
|
/// Stable semantic handle for an interacted element within a page observation.
|
||||||
///
|
|
||||||
/// `name` and `nearby_label` are **untrusted page text**.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct TargetDescriptor {
|
pub struct TargetDescriptor {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none", rename = "ref")]
|
||||||
|
pub element_ref: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub role: Option<String>,
|
pub role: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub tag: String,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub name_attr: Option<String>,
|
pub ctx: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||||
pub placeholder: Option<String>,
|
pub unmatched: bool,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub nearby_label: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -42,15 +40,54 @@ pub struct TraceEntry {
|
|||||||
pub start_url: String,
|
pub start_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Page context dictionary entry — referenced by steps via `page` id.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct PageRef {
|
pub struct RecorderInfo {
|
||||||
|
pub bsk: String,
|
||||||
|
pub vom: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum StopReason {
|
||||||
|
UserFinish,
|
||||||
|
CliStop,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Page observation dictionary entry — referenced by steps via `state` / `result.state`.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct TraceState {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
|
/// Wire-only: full page observation (front matter + VOM body + annotations).
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub body: Option<String>,
|
||||||
|
/// Disk-only: filename under the bundle `pages/` directory.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub page: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||||
|
pub truncated: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct StepResult {
|
||||||
|
pub state: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fields shared by every step variant (flattened in JSON).
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct StepCommon {
|
||||||
|
pub id: u32,
|
||||||
|
/// Observation id immediately before this action.
|
||||||
|
pub state: String,
|
||||||
|
pub result: StepResult,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Step op-specific payloads
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// One selected option (`select` op).
|
/// One selected option (`select` op).
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct SelectedOption {
|
pub struct SelectedOption {
|
||||||
@@ -59,27 +96,26 @@ pub struct SelectedOption {
|
|||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Observed navigation after a step (objective fact only).
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub struct StepEffect {
|
pub enum NavigationCause {
|
||||||
/// Reference into `pages[]` for the destination page.
|
UserTyped,
|
||||||
pub navigated_to: String,
|
Link,
|
||||||
|
FormSubmit,
|
||||||
|
Reload,
|
||||||
|
History,
|
||||||
|
Script,
|
||||||
|
Browser,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fields shared by every step variant (flattened in JSON).
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub struct StepCommon {
|
pub enum FillCommit {
|
||||||
pub id: u32,
|
Enter,
|
||||||
/// Reference into `pages[]`.
|
Suggestion,
|
||||||
pub page: String,
|
Blur,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub effect: Option<StepEffect>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Step op-specific payloads
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// One recorded user action — discriminated union by `op`.
|
/// One recorded user action — discriminated union by `op`.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
#[serde(tag = "op", rename_all = "snake_case")]
|
#[serde(tag = "op", rename_all = "snake_case")]
|
||||||
@@ -88,6 +124,7 @@ pub enum Step {
|
|||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
common: StepCommon,
|
common: StepCommon,
|
||||||
to: String,
|
to: String,
|
||||||
|
cause: NavigationCause,
|
||||||
},
|
},
|
||||||
Click {
|
Click {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
@@ -104,13 +141,15 @@ pub enum Step {
|
|||||||
common: StepCommon,
|
common: StepCommon,
|
||||||
target: TargetDescriptor,
|
target: TargetDescriptor,
|
||||||
value: String,
|
value: String,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
commit: FillCommit,
|
||||||
redacted: Option<bool>,
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||||
|
redacted: bool,
|
||||||
},
|
},
|
||||||
Select {
|
Select {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
common: StepCommon,
|
common: StepCommon,
|
||||||
target: TargetDescriptor,
|
target: TargetDescriptor,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
selection: Vec<SelectedOption>,
|
selection: Vec<SelectedOption>,
|
||||||
},
|
},
|
||||||
Press {
|
Press {
|
||||||
@@ -122,6 +161,10 @@ pub enum Step {
|
|||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
target: Option<TargetDescriptor>,
|
target: Option<TargetDescriptor>,
|
||||||
},
|
},
|
||||||
|
Scroll {
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: StepCommon,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -131,15 +174,16 @@ pub enum Step {
|
|||||||
/// Persisted user-action trace exported by `tool.record_stop` / `await`.
|
/// Persisted user-action trace exported by `tool.record_stop` / `await`.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct Trace {
|
pub struct Trace {
|
||||||
/// RFC 3339 timestamp when recording stopped.
|
pub version: u32,
|
||||||
pub recorded_at: String,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub started_at: Option<String>,
|
|
||||||
/// Optional user-provided goal from `--purpose` (metadata only).
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub purpose: Option<String>,
|
pub purpose: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub started_at: Option<String>,
|
||||||
|
pub recorded_at: String,
|
||||||
|
pub stopped_by: StopReason,
|
||||||
pub entry: TraceEntry,
|
pub entry: TraceEntry,
|
||||||
pub pages: Vec<PageRef>,
|
pub recorder: RecorderInfo,
|
||||||
|
pub states: Vec<TraceState>,
|
||||||
pub steps: Vec<Step>,
|
pub steps: Vec<Step>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +200,10 @@ pub struct RecordStartParams {
|
|||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub purpose: Option<String>,
|
pub purpose: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub max_page_tokens: Option<u32>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub redact_values: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
@@ -195,81 +243,79 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
fn sample_common(id: u32) -> StepCommon {
|
fn sample_common(id: u32, state: &str, result_state: &str) -> StepCommon {
|
||||||
StepCommon {
|
StepCommon {
|
||||||
id,
|
id,
|
||||||
page: "p1".into(),
|
state: state.into(),
|
||||||
effect: None,
|
result: StepResult {
|
||||||
|
state: result_state.into(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sample_target() -> TargetDescriptor {
|
fn sample_target() -> TargetDescriptor {
|
||||||
TargetDescriptor {
|
TargetDescriptor {
|
||||||
|
element_ref: Some("e21".into()),
|
||||||
role: Some("button".into()),
|
role: Some("button".into()),
|
||||||
name: Some("发布".into()),
|
name: Some("发布".into()),
|
||||||
tag: "button".into(),
|
ctx: Some("金桔柠檬 6 号".into()),
|
||||||
name_attr: None,
|
unmatched: false,
|
||||||
placeholder: None,
|
|
||||||
nearby_label: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sample_trace() -> Trace {
|
fn sample_trace() -> Trace {
|
||||||
Trace {
|
Trace {
|
||||||
recorded_at: "2026-07-17T09:01:10Z".into(),
|
version: TRACE_VERSION,
|
||||||
started_at: Some("2026-07-17T09:00:00Z".into()),
|
purpose: Some("把草稿商品发布上架".into()),
|
||||||
purpose: Some("发布一篇文章".into()),
|
started_at: Some("2026-08-10T02:10:41.080Z".into()),
|
||||||
|
recorded_at: "2026-08-10T02:12:55.360Z".into(),
|
||||||
|
stopped_by: StopReason::UserFinish,
|
||||||
entry: TraceEntry {
|
entry: TraceEntry {
|
||||||
start_url: "https://x.com/editor".into(),
|
start_url: "https://example.com/".into(),
|
||||||
},
|
},
|
||||||
pages: vec![
|
recorder: RecorderInfo {
|
||||||
PageRef {
|
bsk: "0.1.10".into(),
|
||||||
id: "p1".into(),
|
vom: VOM_FORMAT_VERSION,
|
||||||
url: "https://x.com/editor".into(),
|
},
|
||||||
title: Some("写文章".into()),
|
states: vec![
|
||||||
|
TraceState {
|
||||||
|
id: "s1".into(),
|
||||||
|
url: "https://example.com/".into(),
|
||||||
|
title: Some("Example Domain".into()),
|
||||||
|
body: None,
|
||||||
|
page: Some("s1.vom.txt".into()),
|
||||||
|
truncated: false,
|
||||||
},
|
},
|
||||||
PageRef {
|
TraceState {
|
||||||
id: "p2".into(),
|
id: "s2".into(),
|
||||||
url: "https://x.com/p/99".into(),
|
url: "https://shop.example.com/products?status=draft".into(),
|
||||||
title: None,
|
title: Some("商品管理".into()),
|
||||||
|
body: None,
|
||||||
|
page: Some("s2.vom.txt".into()),
|
||||||
|
truncated: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
steps: vec![
|
steps: vec![
|
||||||
Step::Fill {
|
Step::Navigate {
|
||||||
common: sample_common(1),
|
common: sample_common(1, "s1", "s2"),
|
||||||
target: TargetDescriptor {
|
to: "https://shop.example.com/products?status=draft".into(),
|
||||||
role: Some("textbox".into()),
|
cause: NavigationCause::UserTyped,
|
||||||
name: Some("标题".into()),
|
|
||||||
tag: "input".into(),
|
|
||||||
name_attr: None,
|
|
||||||
placeholder: None,
|
|
||||||
nearby_label: None,
|
|
||||||
},
|
|
||||||
value: "我的第一篇文章".into(),
|
|
||||||
redacted: None,
|
|
||||||
},
|
},
|
||||||
Step::Select {
|
Step::Fill {
|
||||||
common: sample_common(3),
|
common: sample_common(2, "s2", "s2"),
|
||||||
target: TargetDescriptor {
|
target: TargetDescriptor {
|
||||||
role: Some("combobox".into()),
|
element_ref: Some("e12".into()),
|
||||||
name: Some("分类".into()),
|
role: Some("textbox".into()),
|
||||||
tag: "select".into(),
|
name: Some("搜索商品".into()),
|
||||||
name_attr: None,
|
ctx: None,
|
||||||
placeholder: None,
|
unmatched: false,
|
||||||
nearby_label: None,
|
|
||||||
},
|
},
|
||||||
selection: vec![SelectedOption {
|
value: "金桔柠檬".into(),
|
||||||
value: "tech".into(),
|
commit: FillCommit::Enter,
|
||||||
label: Some("技术分享".into()),
|
redacted: false,
|
||||||
}],
|
|
||||||
},
|
},
|
||||||
Step::Click {
|
Step::Click {
|
||||||
common: StepCommon {
|
common: sample_common(3, "s2", "s2"),
|
||||||
effect: Some(StepEffect {
|
|
||||||
navigated_to: "p2".into(),
|
|
||||||
}),
|
|
||||||
..sample_common(4)
|
|
||||||
},
|
|
||||||
target: sample_target(),
|
target: sample_target(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -279,35 +325,37 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn step_click_round_trips() {
|
fn step_click_round_trips() {
|
||||||
let step = Step::Click {
|
let step = Step::Click {
|
||||||
common: sample_common(1),
|
common: sample_common(1, "s1", "s2"),
|
||||||
target: sample_target(),
|
target: sample_target(),
|
||||||
};
|
};
|
||||||
let v = serde_json::to_value(&step).unwrap();
|
let v = serde_json::to_value(&step).unwrap();
|
||||||
assert_eq!(v.get("op").and_then(|v| v.as_str()), Some("click"));
|
assert_eq!(v.get("op").and_then(|v| v.as_str()), Some("click"));
|
||||||
assert_eq!(v.get("page").and_then(|v| v.as_str()), Some("p1"));
|
assert_eq!(v.get("state").and_then(|v| v.as_str()), Some("s1"));
|
||||||
assert!(v.get("key").is_none());
|
assert_eq!(v["result"]["state"], "s2");
|
||||||
|
assert_eq!(v["target"]["ref"], "e21");
|
||||||
let round: Step = serde_json::from_value(v).unwrap();
|
let round: Step = serde_json::from_value(v).unwrap();
|
||||||
assert_eq!(round, step);
|
assert_eq!(round, step);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn step_fill_with_raw_value_round_trips() {
|
fn step_fill_with_commit_round_trips() {
|
||||||
let step = Step::Fill {
|
let step = Step::Fill {
|
||||||
common: sample_common(2),
|
common: sample_common(2, "s2", "s3"),
|
||||||
target: TargetDescriptor {
|
target: TargetDescriptor {
|
||||||
|
element_ref: Some("e12".into()),
|
||||||
role: Some("textbox".into()),
|
role: Some("textbox".into()),
|
||||||
name: Some("服务名称".into()),
|
name: Some("搜索商品".into()),
|
||||||
tag: "input".into(),
|
ctx: None,
|
||||||
name_attr: Some("serviceName".into()),
|
unmatched: false,
|
||||||
placeholder: None,
|
|
||||||
nearby_label: None,
|
|
||||||
},
|
},
|
||||||
value: "my-svc".into(),
|
value: "browser skill".into(),
|
||||||
redacted: None,
|
commit: FillCommit::Enter,
|
||||||
|
redacted: false,
|
||||||
};
|
};
|
||||||
let v = serde_json::to_value(&step).unwrap();
|
let v = serde_json::to_value(&step).unwrap();
|
||||||
assert_eq!(v["op"], "fill");
|
assert_eq!(v["op"], "fill");
|
||||||
assert_eq!(v["value"], "my-svc");
|
assert_eq!(v["commit"], "enter");
|
||||||
|
assert!(v.get("redacted").is_none());
|
||||||
let round: Step = serde_json::from_value(v).unwrap();
|
let round: Step = serde_json::from_value(v).unwrap();
|
||||||
assert_eq!(round, step);
|
assert_eq!(round, step);
|
||||||
}
|
}
|
||||||
@@ -315,17 +363,17 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn step_fill_password_is_redacted() {
|
fn step_fill_password_is_redacted() {
|
||||||
let step = Step::Fill {
|
let step = Step::Fill {
|
||||||
common: sample_common(1),
|
common: sample_common(1, "s1", "s1"),
|
||||||
target: TargetDescriptor {
|
target: TargetDescriptor {
|
||||||
|
element_ref: Some("e3".into()),
|
||||||
role: Some("textbox".into()),
|
role: Some("textbox".into()),
|
||||||
name: Some("密码".into()),
|
name: Some("密码".into()),
|
||||||
tag: "input".into(),
|
ctx: None,
|
||||||
name_attr: None,
|
unmatched: false,
|
||||||
placeholder: None,
|
|
||||||
nearby_label: None,
|
|
||||||
},
|
},
|
||||||
value: "***".into(),
|
value: "***".into(),
|
||||||
redacted: Some(true),
|
commit: FillCommit::Blur,
|
||||||
|
redacted: true,
|
||||||
};
|
};
|
||||||
let v = serde_json::to_value(&step).unwrap();
|
let v = serde_json::to_value(&step).unwrap();
|
||||||
assert_eq!(v["value"], "***");
|
assert_eq!(v["value"], "***");
|
||||||
@@ -333,131 +381,102 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn step_select_uses_object_array() {
|
fn step_navigate_with_cause_round_trips() {
|
||||||
let step = Step::Select {
|
|
||||||
common: sample_common(3),
|
|
||||||
target: TargetDescriptor {
|
|
||||||
role: Some("combobox".into()),
|
|
||||||
name: Some("分类".into()),
|
|
||||||
tag: "select".into(),
|
|
||||||
name_attr: None,
|
|
||||||
placeholder: None,
|
|
||||||
nearby_label: None,
|
|
||||||
},
|
|
||||||
selection: vec![SelectedOption {
|
|
||||||
value: "tech".into(),
|
|
||||||
label: Some("技术分享".into()),
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
let v = serde_json::to_value(&step).unwrap();
|
|
||||||
assert_eq!(v["selection"][0]["value"], "tech");
|
|
||||||
assert_eq!(v["selection"][0]["label"], "技术分享");
|
|
||||||
let round: Step = serde_json::from_value(v).unwrap();
|
|
||||||
assert_eq!(round, step);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn step_navigate_round_trips() {
|
|
||||||
let step = Step::Navigate {
|
let step = Step::Navigate {
|
||||||
common: sample_common(1),
|
common: sample_common(1, "s1", "s2"),
|
||||||
to: "https://example.com".into(),
|
to: "https://example.com".into(),
|
||||||
|
cause: NavigationCause::UserTyped,
|
||||||
};
|
};
|
||||||
let v = serde_json::to_value(&step).unwrap();
|
let v = serde_json::to_value(&step).unwrap();
|
||||||
assert_eq!(v["op"], "navigate");
|
assert_eq!(v["op"], "navigate");
|
||||||
assert_eq!(v["to"], "https://example.com");
|
assert_eq!(v["cause"], "user_typed");
|
||||||
let round: Step = serde_json::from_value(v).unwrap();
|
let round: Step = serde_json::from_value(v).unwrap();
|
||||||
assert_eq!(round, step);
|
assert_eq!(round, step);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn step_press_with_modifiers_round_trips() {
|
fn trace_v3_round_trips() {
|
||||||
let step = Step::Press {
|
|
||||||
common: StepCommon {
|
|
||||||
effect: Some(StepEffect {
|
|
||||||
navigated_to: "p2".into(),
|
|
||||||
}),
|
|
||||||
..sample_common(2)
|
|
||||||
},
|
|
||||||
key: "Enter".into(),
|
|
||||||
modifiers: Some(vec![KeyModifier::Ctrl, KeyModifier::Shift]),
|
|
||||||
target: Some(sample_target()),
|
|
||||||
};
|
|
||||||
let v = serde_json::to_value(&step).unwrap();
|
|
||||||
assert_eq!(v["key"], "Enter");
|
|
||||||
assert_eq!(v["modifiers"], json!(["ctrl", "shift"]));
|
|
||||||
assert_eq!(v["effect"]["navigated_to"], "p2");
|
|
||||||
let round: Step = serde_json::from_value(v).unwrap();
|
|
||||||
assert_eq!(round, step);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trace_record_only_round_trips() {
|
|
||||||
let trace = sample_trace();
|
let trace = sample_trace();
|
||||||
let v = serde_json::to_value(&trace).unwrap();
|
let v = serde_json::to_value(&trace).unwrap();
|
||||||
assert!(v.get("version").is_none());
|
assert_eq!(v.get("version").and_then(|v| v.as_u64()), Some(3));
|
||||||
assert_eq!(
|
assert!(v.get("pages").is_none());
|
||||||
v.get("purpose").and_then(|v| v.as_str()),
|
assert_eq!(v["recorder"]["vom"], 1);
|
||||||
Some("发布一篇文章")
|
assert_eq!(v["states"].as_array().unwrap().len(), 2);
|
||||||
);
|
|
||||||
assert!(v.get("parameters").is_none());
|
|
||||||
assert!(v.get("site").is_none());
|
|
||||||
assert!(v.get("goal").is_none());
|
|
||||||
assert_eq!(
|
|
||||||
v.get("entry")
|
|
||||||
.and_then(|e| e.get("start_url"))
|
|
||||||
.and_then(|u| u.as_str()),
|
|
||||||
Some("https://x.com/editor")
|
|
||||||
);
|
|
||||||
let round: Trace = serde_json::from_value(v).unwrap();
|
let round: Trace = serde_json::from_value(v).unwrap();
|
||||||
assert_eq!(round, trace);
|
assert_eq!(round, trace);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn discriminated_union_click_ignores_extra_key_field() {
|
fn default_fields_are_omitted() {
|
||||||
let bad = json!({
|
let step = Step::Click {
|
||||||
"op": "click",
|
common: sample_common(1, "s1", "s1"),
|
||||||
"id": 1,
|
target: TargetDescriptor {
|
||||||
"page": "p1",
|
element_ref: Some("e1".into()),
|
||||||
"target": { "tag": "button", "name": "OK" },
|
role: Some("button".into()),
|
||||||
"key": "Enter"
|
name: Some("OK".into()),
|
||||||
});
|
ctx: None,
|
||||||
let step: Step = serde_json::from_value(bad).unwrap();
|
unmatched: false,
|
||||||
assert!(matches!(step, Step::Click { .. }));
|
},
|
||||||
|
};
|
||||||
let v = serde_json::to_value(&step).unwrap();
|
let v = serde_json::to_value(&step).unwrap();
|
||||||
assert!(v.get("key").is_none());
|
assert!(v.get("unmatched").is_none());
|
||||||
|
assert!(v["target"].get("ctx").is_none());
|
||||||
|
assert!(v["target"].get("unmatched").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unmatched_target_serializes_flag() {
|
||||||
|
let step = Step::Click {
|
||||||
|
common: sample_common(1, "s1", "s2"),
|
||||||
|
target: TargetDescriptor {
|
||||||
|
element_ref: None,
|
||||||
|
role: Some("button".into()),
|
||||||
|
name: Some("发布".into()),
|
||||||
|
ctx: None,
|
||||||
|
unmatched: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let v = serde_json::to_value(&step).unwrap();
|
||||||
|
assert_eq!(v["target"]["unmatched"], true);
|
||||||
|
assert!(v["target"].get("ref").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn extension_trace_deserializes() {
|
fn extension_trace_deserializes() {
|
||||||
let v = json!({
|
let v = json!({
|
||||||
|
"version": 3,
|
||||||
"recorded_at": "2026-07-21T08:00:00Z",
|
"recorded_at": "2026-07-21T08:00:00Z",
|
||||||
"started_at": "2026-07-21T07:59:00Z",
|
"started_at": "2026-07-21T07:59:00Z",
|
||||||
"purpose": "demo",
|
"purpose": "demo",
|
||||||
|
"stopped_by": "user_finish",
|
||||||
"entry": { "start_url": "https://example.com/editor" },
|
"entry": { "start_url": "https://example.com/editor" },
|
||||||
"pages": [
|
"recorder": { "bsk": "0.1.10", "vom": 1 },
|
||||||
{ "id": "p1", "url": "https://example.com/editor" },
|
"states": [
|
||||||
{ "id": "p2", "url": "https://example.com/p/99" }
|
{ "id": "s1", "url": "https://example.com/editor", "page": "s1.vom.txt" },
|
||||||
|
{ "id": "s2", "url": "https://example.com/p/99", "page": "s2.vom.txt" }
|
||||||
],
|
],
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"op": "fill",
|
"op": "fill",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"page": "p1",
|
"state": "s1",
|
||||||
"target": { "tag": "input", "role": "textbox", "name": "标题" },
|
"result": { "state": "s1" },
|
||||||
"value": "hello"
|
"target": { "ref": "e1", "role": "textbox", "name": "标题" },
|
||||||
|
"value": "hello",
|
||||||
|
"commit": "blur"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"op": "click",
|
"op": "click",
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"page": "p1",
|
"state": "s1",
|
||||||
"target": { "tag": "button", "role": "button", "name": "发布" },
|
"result": { "state": "s2" },
|
||||||
"effect": { "navigated_to": "p2" }
|
"target": { "ref": "e2", "role": "button", "name": "发布" }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
let trace: Trace = serde_json::from_value(v).unwrap();
|
let trace: Trace = serde_json::from_value(v).unwrap();
|
||||||
assert_eq!(trace.entry.start_url, "https://example.com/editor");
|
assert_eq!(trace.version, 3);
|
||||||
assert_eq!(trace.pages.len(), 2);
|
assert_eq!(trace.states.len(), 2);
|
||||||
assert_eq!(trace.steps.len(), 2);
|
assert_eq!(trace.steps.len(), 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user