fix(editor): restore release branch checks

This commit is contained in:
Kayshen-X
2026-08-25 22:15:15 +08:00
parent b2d3f605a6
commit 4e88df9913
13 changed files with 347 additions and 314 deletions
+2
View File
@@ -25,6 +25,7 @@ on:
- 'crates/op-editor-ui/**'
- 'crates/op-host-native/**'
- 'crates/op-host-desktop/**'
- 'crates/op-chat-agent/src/provider_dial.rs'
- 'crates/op-host-services/**'
- 'crates/op-i18n/**'
- 'deploy/collab-relay/**'
@@ -68,6 +69,7 @@ on:
- 'crates/op-editor-ui/**'
- 'crates/op-host-native/**'
- 'crates/op-host-desktop/**'
- 'crates/op-chat-agent/src/provider_dial.rs'
- 'crates/op-host-services/**'
- 'crates/op-i18n/**'
- 'deploy/collab-relay/**'
+2 -35
View File
@@ -16,6 +16,7 @@
pub mod chrome;
mod defaults;
mod exports;
pub mod git_panel;
pub mod groups;
mod methods;
@@ -24,45 +25,11 @@ pub mod slides_panel_state;
#[cfg(test)]
mod tests;
pub use chrome::{
DesignMdRequest, EmbedHost, FileAction, PencilCursorStyle, RecentFile, ThemeMode,
ThemePresetIo, UpdateStatus, WindowControlRequest, RECENT_FILE_CAP,
};
pub use git_panel::{
CloneField, CloneFormState, CommitDiffPatch, CommitDiffSummary, CommitDiffView,
GitBranchPickerMode, GitCandidateFile, GitCommitSummary, GitDiffTarget, GitDiffView,
GitFileEntry, GitOverflowView, GitPanelAction, GitPanelState, MergeConflictRow,
MergeResolveFile, MergeResolveState,
};
pub use groups::{
AssetCenterTab, CustomPrompt, DesignMdPanelState, PreviewState, PromptCenterFocus,
PromptCenterState, PromptFilter, SaveNameDialogState, SceneFilter, SceneTemplateCenterState,
SceneTemplateFocus, SizeToggleState, StyleImportState,
};
pub use pickers::{
CanvasDropIndicator, CanvasOverlayLine, CanvasOverlayRect, CompositingPickerTarget,
EffectParamFocus, FontPickerPurpose, LayerContextMenuState, MissingFontSurface,
PageRenameState, PreviewDeviceKind, VariableRowFocus,
};
pub use slides_panel_state::{LeftPanelTab, SlidesDrag, SlidesPanelState, SlidesPanelTarget};
pub use exports::*;
use crate::node_id::NodeId;
use crate::tool::Tool;
use std::collections::HashSet;
// `Locale` is the i18n locale enum — dependency-free + wasm-clean, so
// it lives in `op-i18n` and re-exports cleanly into the state layer.
pub use op_i18n::Locale;
pub use crate::property_panel_state::{
BooleanOp, ExportFormat, FillType, FlexLayout, ImageAdjustmentField, ImageFillMode,
PaddingEditMode, PropertyTab,
};
// What the LayerPanel right-click context menu is acting on — the
// canonical definition is `ui_draft::LayerContextTarget` (it backs
// the inline-rename draft too). Re-exported so UI code that
// references a context target has one import path.
pub use crate::ui_draft::LayerContextTarget;
/// Editor-UI overlay + panel state — the widget-layer toggles, hover
/// targets, menu / modal open flags and panel metrics that the ~30
@@ -0,0 +1,40 @@
//! Public re-exports for the `editor_ui_state` spine.
pub use super::chrome::{
DesignMdRequest, EmbedHost, FileAction, PencilCursorStyle, RecentFile, ThemeMode,
ThemePresetIo, UpdateStatus, WindowControlRequest, RECENT_FILE_CAP,
};
pub use super::git_panel::{
CloneField, CloneFormState, CommitDiffPatch, CommitDiffSummary, CommitDiffView,
GitBranchPickerMode, GitCandidateFile, GitCommitSummary, GitDiffTarget, GitDiffView,
GitFileEntry, GitOverflowView, GitPanelAction, GitPanelState, MergeConflictRow,
MergeResolveFile, MergeResolveState,
};
pub use super::groups::{
AssetCenterTab, CustomPrompt, DesignMdPanelState, PreviewState, PromptCenterFocus,
PromptCenterState, PromptFilter, SaveNameDialogState, SceneFilter, SceneTemplateCenterState,
SceneTemplateFocus, SizeToggleState, StyleImportState,
};
pub use super::pickers::{
CanvasDropIndicator, CanvasOverlayLine, CanvasOverlayRect, CompositingPickerTarget,
EffectParamFocus, FontPickerPurpose, LayerContextMenuState, MissingFontSurface,
PageRenameState, PreviewDeviceKind, VariableRowFocus,
};
pub use super::slides_panel_state::{
LeftPanelTab, SlidesDrag, SlidesPanelState, SlidesPanelTarget,
};
// `Locale` is the i18n locale enum — dependency-free + wasm-clean, so
// it lives in `op-i18n` and re-exports cleanly into the state layer.
pub use op_i18n::Locale;
pub use crate::property_panel_state::{
BooleanOp, ExportFormat, FillType, FlexLayout, ImageAdjustmentField, ImageFillMode,
PaddingEditMode, PropertyTab,
};
// What the LayerPanel right-click context menu is acting on — the
// canonical definition is `ui_draft::LayerContextTarget` (it backs
// the inline-rename draft too). Re-exported so UI code that
// references a context target has one import path.
pub use crate::ui_draft::LayerContextTarget;
+2 -263
View File
@@ -551,266 +551,5 @@ fn active_page_id(state: &crate::EditorState) -> Option<String> {
mod editor_meta_tests;
#[cfg(test)]
mod tests {
use super::*;
const V3: &str = r#"{"document":{"version":"1.0","children":[]},"version":3}"#;
#[test]
fn next_document_offers_the_first_response() {
let c = WebSyncClient::new();
assert!(matches!(c.next_document(V3), Ok(Some((_, 3)))));
// Read-only: not committed until mark_applied.
assert_eq!(c.applied_version(), 0);
}
#[test]
fn mark_applied_then_skips_equal_or_older_versions() {
let mut c = WebSyncClient::new();
assert!(c.next_document(V3).expect("ok").is_some());
c.mark_applied(3);
// Same version → nothing newer.
assert!(c.next_document(V3).expect("ok").is_none());
// Older version → nothing newer.
let older = r#"{"document":{"version":"1.0","children":[]},"version":2}"#;
assert!(c.next_document(older).expect("ok").is_none());
assert_eq!(c.applied_version(), 3);
}
#[test]
fn next_document_offers_a_newer_version_after_commit() {
let mut c = WebSyncClient::new();
c.mark_applied(3);
let newer = r#"{"document":{"version":"1.0","children":[]},"version":5}"#;
assert!(matches!(c.next_document(newer), Ok(Some((_, 5)))));
}
#[test]
fn uncommitted_version_is_re_offered_so_a_failed_repaint_is_not_lost() {
// Decide-then-commit: if the caller does NOT mark_applied (e.g. repaint
// failed), the same newer version must still be offered next poll.
let c = WebSyncClient::new();
assert!(c.next_document(V3).expect("ok").is_some());
// No mark_applied → still offered.
assert!(c.next_document(V3).expect("ok").is_some());
}
#[test]
fn sync_commits_only_on_apply_success_and_never_stale() {
let mut c = WebSyncClient::new();
// apply succeeds → commits exactly the applied version (3).
let mut applied_version = None;
assert!(c
.sync(V3, |_doc, v| {
applied_version = Some(v);
true
})
.expect("ok"));
assert_eq!(applied_version, Some(3));
assert_eq!(c.applied_version(), 3);
// nothing newer → apply callback not invoked.
let mut called = false;
assert!(!c
.sync(V3, |_d, _v| {
called = true;
true
})
.expect("ok"));
assert!(!called);
// newer, but apply (repaint) FAILS → NOT committed (stays 3), retried.
let v5 = r#"{"document":{"version":"1.0","children":[]},"version":5}"#;
assert!(!c.sync(v5, |_d, _v| false).expect("ok"));
assert_eq!(c.applied_version(), 3);
// retry succeeds → commits 5.
assert!(c.sync(v5, |_d, _v| true).expect("ok"));
assert_eq!(c.applied_version(), 5);
}
#[test]
fn metadata_aware_sync_applies_preserve_mode_before_committing_version() {
let mut c = WebSyncClient::new();
let body = r#"{"document":{"version":"1.0","children":[]},"version":8,"preserveAuthoredGeometry":true}"#;
let mut applied = None;
assert!(c
.sync_with_metadata(body, |_doc, version, preserve| {
applied = Some((version, preserve));
true
})
.expect("valid response"));
assert_eq!(applied, Some((8, true)));
assert_eq!(c.applied_version(), 8);
}
#[test]
fn next_document_rejects_malformed_responses() {
let c = WebSyncClient::new();
assert!(c.next_document("not json").is_err());
// Missing version.
assert!(c
.next_document(r#"{"document":{"version":"1.0","children":[]}}"#)
.is_err());
// Missing document on a first (would-apply) response.
assert!(c.next_document(r#"{"version":1}"#).is_err());
}
#[test]
fn build_push_body_wraps_the_document() {
let doc: PenDocument =
serde_json::from_str(r#"{"version":"1.0","children":[]}"#).expect("doc");
let body = WebSyncClient::build_push_body(&doc).expect("body");
assert!(body.starts_with(r#"{"document":"#), "{body}");
assert!(body.contains(r#""version":"1.0""#), "{body}");
// Round-trips back through the daemon's request parser shape.
let value: serde_json::Value = serde_json::from_str(&body).expect("valid json");
assert!(value.get("document").is_some());
// The wrap helper produces the identical body from the same JSON.
let doc_json = serde_json::to_string(&doc).expect("doc json");
assert_eq!(WebSyncClient::wrap_push_body(&doc_json), body);
}
#[test]
fn version_probe_gates_the_document_fetch() {
let mut c = WebSyncClient::new();
// First sync: any version (even 0) warrants a fetch.
assert!(c.wants_version(0));
c.mark_applied(3);
assert!(!c.wants_version(2));
assert!(!c.wants_version(3));
assert!(c.wants_version(4));
// Probe body parsing.
assert_eq!(
WebSyncClient::parse_version_probe(r#"{"version":7}"#),
Some(7)
);
assert_eq!(WebSyncClient::parse_version_probe(r#"{"ok":false}"#), None);
assert_eq!(WebSyncClient::parse_version_probe("not json"), None);
}
#[test]
fn push_is_gated_on_first_sync_and_content_change() {
let mut c = WebSyncClient::new();
let starter = r#"{"version":"1.0","children":[]}"#;
// Before the first daemon apply the browser must NOT push its
// boot-time starter document over the daemon's post-reset authority.
assert!(!c.initialized());
assert!(!c.should_push(starter));
// Apply the daemon doc, note its local serialization as baseline.
assert!(c.sync(V3, |_d, _v| true).expect("ok"));
c.note_applied_snapshot(starter);
// Unchanged content → no push (the echo-suppression core).
assert!(!c.should_push(starter));
// A real local edit changes the serialization → push.
let edited = r#"{"version":"1.0","children":[{"id":"n1"}]}"#;
assert!(c.should_push(edited));
}
#[test]
fn bootstrap_push_is_disabled_before_the_first_daemon_apply() {
let mut c = WebSyncClient::new();
let starter = r#"{"version":"1.0","children":[]}"#;
let edited = r#"{"version":"1.0","children":[{"id":"n1"}]}"#;
assert!(!c.should_bootstrap_push(starter, starter));
assert!(!c.should_bootstrap_push(edited, starter));
c.mark_pushed(edited, 1);
assert!(!c.should_bootstrap_push(edited, starter));
}
#[test]
fn mark_pushed_commits_baseline_and_version_so_echo_is_skipped() {
let mut c = WebSyncClient::new();
assert!(c.sync(V3, |_d, _v| true).expect("ok"));
let edited = r#"{"version":"1.0","children":[{"id":"n1"}]}"#;
assert!(c.should_push(edited));
// Daemon accepted the push as version 4.
c.mark_pushed(edited, 4);
assert_eq!(c.applied_version(), 4);
// Neither the content nor the version is re-offered (no echo).
assert!(!c.should_push(edited));
assert!(!c.wants_version(4));
let echo = r#"{"document":{"version":"1.0","children":[]},"version":4}"#;
assert!(c.next_document(echo).expect("ok").is_none());
// A later external version still syncs.
assert!(c.wants_version(5));
}
#[test]
fn push_response_parses_only_the_ok_shape() {
assert_eq!(
WebSyncClient::parse_push_response(r#"{"ok":true,"version":9}"#),
Some(9)
);
assert_eq!(
WebSyncClient::parse_push_response(r#"{"ok":false,"error":"x"}"#),
None
);
assert_eq!(WebSyncClient::parse_push_response(r#"{"version":9}"#), None);
assert_eq!(WebSyncClient::parse_push_response(""), None);
}
#[test]
fn push_body_with_base_and_conflict_roundtrip() {
let body = WebSyncClient::wrap_push_body_with_base(r#"{"pages":[]}"#, 7);
assert!(body.contains(r#""baseVersion":7"#));
assert_eq!(
WebSyncClient::parse_push_conflict(
r#"{"ok":false,"error":"version-conflict","version":12}"#
),
Some(12)
);
assert_eq!(
WebSyncClient::parse_push_conflict(r#"{"ok":true,"version":9}"#),
None
);
}
#[test]
fn metadata_aware_push_adds_preserve_mode_without_changing_legacy_helpers() {
let doc = r#"{"version":"1.0","children":[]}"#;
let preserved = WebSyncClient::wrap_push_body_with_base_and_preserve(doc, 7, true);
let value: serde_json::Value = serde_json::from_str(&preserved).expect("push json");
assert_eq!(value["baseVersion"], 7);
assert_eq!(value["preserveAuthoredGeometry"], true);
let legacy = WebSyncClient::wrap_push_body_with_base(doc, 7);
let legacy_value: serde_json::Value = serde_json::from_str(&legacy).expect("legacy json");
assert!(legacy_value.get("preserveAuthoredGeometry").is_none());
}
#[test]
fn selection_key_and_body_track_ids_and_active_page() {
let mut state = crate::EditorState::new();
let key_empty = selection_sync_key(&state);
assert_eq!(key_empty, "sel:|page:");
state.doc.children = vec![];
state.selection.set = vec![crate::NodeId::new("n1"), crate::NodeId::new("n2")];
state.selection.anchor = crate::NodeId::new("n2");
let key = selection_sync_key(&state);
assert_eq!(key, "sel:n1,n2|page:");
assert_ne!(key, key_empty);
// Body matches the TS selection.post.ts renderer shape.
let body: serde_json::Value =
serde_json::from_str(&selection_push_body(&state)).expect("json");
assert_eq!(body["selectedIds"], serde_json::json!(["n1", "n2"]));
assert_eq!(body["activePageId"], serde_json::Value::Null);
}
#[test]
fn selection_body_carries_the_active_page_id() {
let doc: PenDocument = serde_json::from_str(
r#"{"version":"1.0","children":[],"pages":[
{"id":"p1","name":"One","children":[]},
{"id":"p2","name":"Two","children":[]}
]}"#,
)
.expect("doc");
let mut state = crate::EditorState::from_document(doc);
assert!(selection_sync_key(&state).ends_with("|page:p1"));
assert!(state.set_active_page(1));
assert!(selection_sync_key(&state).ends_with("|page:p2"));
let body: serde_json::Value =
serde_json::from_str(&selection_push_body(&state)).expect("json");
assert_eq!(body["activePageId"], "p2");
}
}
#[path = "web_sync_tests.rs"]
mod tests;
+258
View File
@@ -0,0 +1,258 @@
use super::*;
const V3: &str = r#"{"document":{"version":"1.0","children":[]},"version":3}"#;
#[test]
fn next_document_offers_the_first_response() {
let c = WebSyncClient::new();
assert!(matches!(c.next_document(V3), Ok(Some((_, 3)))));
// Read-only: not committed until mark_applied.
assert_eq!(c.applied_version(), 0);
}
#[test]
fn mark_applied_then_skips_equal_or_older_versions() {
let mut c = WebSyncClient::new();
assert!(c.next_document(V3).expect("ok").is_some());
c.mark_applied(3);
// Same version → nothing newer.
assert!(c.next_document(V3).expect("ok").is_none());
// Older version → nothing newer.
let older = r#"{"document":{"version":"1.0","children":[]},"version":2}"#;
assert!(c.next_document(older).expect("ok").is_none());
assert_eq!(c.applied_version(), 3);
}
#[test]
fn next_document_offers_a_newer_version_after_commit() {
let mut c = WebSyncClient::new();
c.mark_applied(3);
let newer = r#"{"document":{"version":"1.0","children":[]},"version":5}"#;
assert!(matches!(c.next_document(newer), Ok(Some((_, 5)))));
}
#[test]
fn uncommitted_version_is_re_offered_so_a_failed_repaint_is_not_lost() {
// Decide-then-commit: if the caller does NOT mark_applied (e.g. repaint
// failed), the same newer version must still be offered next poll.
let c = WebSyncClient::new();
assert!(c.next_document(V3).expect("ok").is_some());
// No mark_applied → still offered.
assert!(c.next_document(V3).expect("ok").is_some());
}
#[test]
fn sync_commits_only_on_apply_success_and_never_stale() {
let mut c = WebSyncClient::new();
// apply succeeds → commits exactly the applied version (3).
let mut applied_version = None;
assert!(c
.sync(V3, |_doc, v| {
applied_version = Some(v);
true
})
.expect("ok"));
assert_eq!(applied_version, Some(3));
assert_eq!(c.applied_version(), 3);
// nothing newer → apply callback not invoked.
let mut called = false;
assert!(!c
.sync(V3, |_d, _v| {
called = true;
true
})
.expect("ok"));
assert!(!called);
// newer, but apply (repaint) FAILS → NOT committed (stays 3), retried.
let v5 = r#"{"document":{"version":"1.0","children":[]},"version":5}"#;
assert!(!c.sync(v5, |_d, _v| false).expect("ok"));
assert_eq!(c.applied_version(), 3);
// retry succeeds → commits 5.
assert!(c.sync(v5, |_d, _v| true).expect("ok"));
assert_eq!(c.applied_version(), 5);
}
#[test]
fn metadata_aware_sync_applies_preserve_mode_before_committing_version() {
let mut c = WebSyncClient::new();
let body = r#"{"document":{"version":"1.0","children":[]},"version":8,"preserveAuthoredGeometry":true}"#;
let mut applied = None;
assert!(c
.sync_with_metadata(body, |_doc, version, preserve| {
applied = Some((version, preserve));
true
})
.expect("valid response"));
assert_eq!(applied, Some((8, true)));
assert_eq!(c.applied_version(), 8);
}
#[test]
fn next_document_rejects_malformed_responses() {
let c = WebSyncClient::new();
assert!(c.next_document("not json").is_err());
// Missing version.
assert!(c
.next_document(r#"{"document":{"version":"1.0","children":[]}}"#)
.is_err());
// Missing document on a first (would-apply) response.
assert!(c.next_document(r#"{"version":1}"#).is_err());
}
#[test]
fn build_push_body_wraps_the_document() {
let doc: PenDocument = serde_json::from_str(r#"{"version":"1.0","children":[]}"#).expect("doc");
let body = WebSyncClient::build_push_body(&doc).expect("body");
assert!(body.starts_with(r#"{"document":"#), "{body}");
assert!(body.contains(r#""version":"1.0""#), "{body}");
// Round-trips back through the daemon's request parser shape.
let value: serde_json::Value = serde_json::from_str(&body).expect("valid json");
assert!(value.get("document").is_some());
// The wrap helper produces the identical body from the same JSON.
let doc_json = serde_json::to_string(&doc).expect("doc json");
assert_eq!(WebSyncClient::wrap_push_body(&doc_json), body);
}
#[test]
fn version_probe_gates_the_document_fetch() {
let mut c = WebSyncClient::new();
// First sync: any version (even 0) warrants a fetch.
assert!(c.wants_version(0));
c.mark_applied(3);
assert!(!c.wants_version(2));
assert!(!c.wants_version(3));
assert!(c.wants_version(4));
// Probe body parsing.
assert_eq!(
WebSyncClient::parse_version_probe(r#"{"version":7}"#),
Some(7)
);
assert_eq!(WebSyncClient::parse_version_probe(r#"{"ok":false}"#), None);
assert_eq!(WebSyncClient::parse_version_probe("not json"), None);
}
#[test]
fn push_is_gated_on_first_sync_and_content_change() {
let mut c = WebSyncClient::new();
let starter = r#"{"version":"1.0","children":[]}"#;
// Before the first daemon apply the browser must NOT push its
// boot-time starter document over the daemon's post-reset authority.
assert!(!c.initialized());
assert!(!c.should_push(starter));
// Apply the daemon doc, note its local serialization as baseline.
assert!(c.sync(V3, |_d, _v| true).expect("ok"));
c.note_applied_snapshot(starter);
// Unchanged content → no push (the echo-suppression core).
assert!(!c.should_push(starter));
// A real local edit changes the serialization → push.
let edited = r#"{"version":"1.0","children":[{"id":"n1"}]}"#;
assert!(c.should_push(edited));
}
#[test]
fn bootstrap_push_is_disabled_before_the_first_daemon_apply() {
let mut c = WebSyncClient::new();
let starter = r#"{"version":"1.0","children":[]}"#;
let edited = r#"{"version":"1.0","children":[{"id":"n1"}]}"#;
assert!(!c.should_bootstrap_push(starter, starter));
assert!(!c.should_bootstrap_push(edited, starter));
c.mark_pushed(edited, 1);
assert!(!c.should_bootstrap_push(edited, starter));
}
#[test]
fn mark_pushed_commits_baseline_and_version_so_echo_is_skipped() {
let mut c = WebSyncClient::new();
assert!(c.sync(V3, |_d, _v| true).expect("ok"));
let edited = r#"{"version":"1.0","children":[{"id":"n1"}]}"#;
assert!(c.should_push(edited));
// Daemon accepted the push as version 4.
c.mark_pushed(edited, 4);
assert_eq!(c.applied_version(), 4);
// Neither the content nor the version is re-offered (no echo).
assert!(!c.should_push(edited));
assert!(!c.wants_version(4));
let echo = r#"{"document":{"version":"1.0","children":[]},"version":4}"#;
assert!(c.next_document(echo).expect("ok").is_none());
// A later external version still syncs.
assert!(c.wants_version(5));
}
#[test]
fn push_response_parses_only_the_ok_shape() {
assert_eq!(
WebSyncClient::parse_push_response(r#"{"ok":true,"version":9}"#),
Some(9)
);
assert_eq!(
WebSyncClient::parse_push_response(r#"{"ok":false,"error":"x"}"#),
None
);
assert_eq!(WebSyncClient::parse_push_response(r#"{"version":9}"#), None);
assert_eq!(WebSyncClient::parse_push_response(""), None);
}
#[test]
fn push_body_with_base_and_conflict_roundtrip() {
let body = WebSyncClient::wrap_push_body_with_base(r#"{"pages":[]}"#, 7);
assert!(body.contains(r#""baseVersion":7"#));
assert_eq!(
WebSyncClient::parse_push_conflict(
r#"{"ok":false,"error":"version-conflict","version":12}"#
),
Some(12)
);
assert_eq!(
WebSyncClient::parse_push_conflict(r#"{"ok":true,"version":9}"#),
None
);
}
#[test]
fn metadata_aware_push_adds_preserve_mode_without_changing_legacy_helpers() {
let doc = r#"{"version":"1.0","children":[]}"#;
let preserved = WebSyncClient::wrap_push_body_with_base_and_preserve(doc, 7, true);
let value: serde_json::Value = serde_json::from_str(&preserved).expect("push json");
assert_eq!(value["baseVersion"], 7);
assert_eq!(value["preserveAuthoredGeometry"], true);
let legacy = WebSyncClient::wrap_push_body_with_base(doc, 7);
let legacy_value: serde_json::Value = serde_json::from_str(&legacy).expect("legacy json");
assert!(legacy_value.get("preserveAuthoredGeometry").is_none());
}
#[test]
fn selection_key_and_body_track_ids_and_active_page() {
let mut state = crate::EditorState::new();
let key_empty = selection_sync_key(&state);
assert_eq!(key_empty, "sel:|page:");
state.doc.children = vec![];
state.selection.set = vec![crate::NodeId::new("n1"), crate::NodeId::new("n2")];
state.selection.anchor = crate::NodeId::new("n2");
let key = selection_sync_key(&state);
assert_eq!(key, "sel:n1,n2|page:");
assert_ne!(key, key_empty);
// Body matches the TS selection.post.ts renderer shape.
let body: serde_json::Value = serde_json::from_str(&selection_push_body(&state)).expect("json");
assert_eq!(body["selectedIds"], serde_json::json!(["n1", "n2"]));
assert_eq!(body["activePageId"], serde_json::Value::Null);
}
#[test]
fn selection_body_carries_the_active_page_id() {
let doc: PenDocument = serde_json::from_str(
r#"{"version":"1.0","children":[],"pages":[
{"id":"p1","name":"One","children":[]},
{"id":"p2","name":"Two","children":[]}
]}"#,
)
.expect("doc");
let mut state = crate::EditorState::from_document(doc);
assert!(selection_sync_key(&state).ends_with("|page:p1"));
assert!(state.set_active_page(1));
assert!(selection_sync_key(&state).ends_with("|page:p2"));
let body: serde_json::Value = serde_json::from_str(&selection_push_body(&state)).expect("json");
assert_eq!(body["activePageId"], "p2");
}
-3
View File
@@ -796,6 +796,3 @@ impl WidgetHostNative {
}
}
}
/// Grace period after pan/zoom before full-quality painting resumes.
const INTERACTION_HOT_MS: u64 = 150;
@@ -7,6 +7,9 @@
use super::*;
/// Grace period after pan/zoom before full-quality painting resumes.
pub(in crate::widget_host) const INTERACTION_HOT_MS: u64 = 150;
impl WidgetHostNative {
/// Drain a queued Component-Browser insert: place the chosen
/// UIKit component at the viewport's centre (top-left = centre −
@@ -238,9 +238,9 @@ fn canvas_pan_gesture_opens_and_closes_the_interactive_degrade_window() {
let deadline = host
.next_animation_deadline_ms()
.expect("hot gesture schedules a wake-up");
assert!(deadline <= 1_000 + super::INTERACTION_HOT_MS);
assert!(deadline <= 1_000 + super::host_requests::INTERACTION_HOT_MS);
host.set_now_ms(1_000 + super::INTERACTION_HOT_MS);
host.set_now_ms(1_000 + super::host_requests::INTERACTION_HOT_MS);
assert!(!host.fast_interaction_active());
}
+1 -1
View File
@@ -768,7 +768,7 @@ pub(crate) fn test_measure() -> Rc<dyn MeasureBackend> {
Rc::new(jian_skia::SkiaMeasure::new())
}
#[cfg(test)]
#[cfg(all(test, not(target_os = "windows")))]
pub(crate) mod font_registry_test_support {
use std::sync::{LazyLock, Mutex, MutexGuard};
@@ -151,6 +151,7 @@ for workflow_path in \
"crates/op-editor-ui/**" \
"crates/op-host-native/**" \
"crates/op-host-desktop/**" \
"crates/op-chat-agent/src/provider_dial.rs" \
"crates/op-host-services/**" \
"crates/op-i18n/**" \
"deploy/collab-relay/**" \
@@ -21,6 +21,7 @@ pull_request:
- 'crates/op-editor-ui/**'
- 'crates/op-host-native/**'
- 'crates/op-host-desktop/**'
- 'crates/op-chat-agent/src/provider_dial.rs'
- 'crates/op-host-services/**'
- 'crates/op-i18n/**'
- 'deploy/collab-relay/**'
@@ -51,6 +52,7 @@ push:
- 'crates/op-editor-ui/**'
- 'crates/op-host-native/**'
- 'crates/op-host-desktop/**'
- 'crates/op-chat-agent/src/provider_dial.rs'
- 'crates/op-host-services/**'
- 'crates/op-i18n/**'
- 'deploy/collab-relay/**'
@@ -391,10 +393,26 @@ expect_failure "requires public-only desktop collaboration avatar delegation" \
"desktop avatar security-policy delegation"
new_fixture avatar-proxy-bypass-removed
: > "$fixture_root/crates/op-host-services/src/provider_dial.rs"
: > "$fixture_root/crates/op-chat-agent/src/provider_dial.rs"
cat > "$fixture_root/crates/op-host-services/src/provider_dial.rs" <<'EOF'
fn fake_pinned_client() {
let _ = ".no_proxy()";
let _ = ".resolve_to_addrs";
}
EOF
expect_failure "requires proxy-free pinned avatar dialing" \
"public HTTPS proxy bypass prevention"
new_fixture avatar-dns-pinning-removed
sed '/\.resolve_to_addrs/d' \
"$fixture_root/crates/op-chat-agent/src/provider_dial.rs" \
> "$fixture_root/crates/op-chat-agent/src/provider_dial.rs.next"
mv \
"$fixture_root/crates/op-chat-agent/src/provider_dial.rs.next" \
"$fixture_root/crates/op-chat-agent/src/provider_dial.rs"
expect_failure "requires connect-time DNS pinning for public avatar dialing" \
"public HTTPS DNS pinning"
new_fixture auth-artifact-integrity-removed
: > "$fixture_root/crates/op-auth-bridge/build.rs"
expect_failure "requires authentication artifact integrity verification" \
@@ -422,9 +440,9 @@ awk 'BEGIN { for (line = 1; line <= 801; line++) print "// integration line" }'
expect_failure "enforces the line cap across collaboration integration source" \
"has 801 lines; maximum is 800"
new_fixture missing-workflow-trigger
new_fixture missing-provider-dial-workflow-trigger
awk '
!removed && index($0, "crates/op-host-desktop/**") {
!removed && index($0, "crates/op-chat-agent/src/provider_dial.rs") {
removed = 1
next
}
@@ -435,7 +453,7 @@ awk '
mv \
"$fixture_root/.github/workflows/collab-security.yml.next" \
"$fixture_root/.github/workflows/collab-security.yml"
expect_failure "rejects removal of either integration workflow trigger" \
expect_failure "requires both canonical provider-dial workflow triggers" \
"collaboration security workflow path trigger"
new_fixture relay-edge-mtls-verification-removed
+3 -2
View File
@@ -35,6 +35,7 @@ collab_scan_roots=(
crates/op-collab-host/src
crates/op-host-native/src
crates/op-host-desktop/src
crates/op-chat-agent/src/provider_dial.rs
crates/op-host-services/src/profile_avatar_fetch.rs
crates/op-host-services/src/public_https_client.rs
crates/op-host-services/src/provider_dial.rs
@@ -586,9 +587,9 @@ for desktop_avatar_anchor in \
require_literal crates/op-host-desktop/src/collab_avatar_host.rs \
"$desktop_avatar_anchor" "desktop avatar security-policy delegation"
done
require_literal crates/op-host-services/src/provider_dial.rs \
require_literal crates/op-chat-agent/src/provider_dial.rs \
".no_proxy()" "public HTTPS proxy bypass prevention"
require_literal crates/op-host-services/src/provider_dial.rs \
require_literal crates/op-chat-agent/src/provider_dial.rs \
".resolve_to_addrs" "public HTTPS DNS pinning"
require_literal crates/op-editor-ui/src/collab_avatar_runtime.rs \
"MAX_AVATAR_SOURCE_PIXELS" "decoded avatar pixel limit"
+11 -4
View File
@@ -55,6 +55,7 @@ new_fixture() {
"$fixture_root/crates/op-editor-ui/src" \
"$fixture_root/crates/op-host-native/src" \
"$fixture_root/crates/op-host-desktop/src" \
"$fixture_root/crates/op-chat-agent/src" \
"$fixture_root/crates/op-collab-host/src/runtime/network" \
"$fixture_root/crates/op-host-services/src" \
"$fixture_root/crates/op-i18n/src" \
@@ -756,11 +757,17 @@ EOF
pub fn public_https_client() {}
EOF
cat > "$fixture_root/crates/op-host-services/src/provider_dial.rs" <<'EOF'
fn pinned_client() {
let _ = ".no_proxy()";
let _ = ".resolve_to_addrs";
cat > "$fixture_root/crates/op-chat-agent/src/provider_dial.rs" <<'EOF'
fn pinned_client(builder: reqwest::ClientBuilder, host: &str, addrs: &[SocketAddr]) {
let _client = builder
.no_proxy()
.resolve_to_addrs(host, addrs)
.build();
}
EOF
cat > "$fixture_root/crates/op-host-services/src/provider_dial.rs" <<'EOF'
pub(crate) use op_chat_agent::provider_dial::client_for;
EOF
cat > "$fixture_root/crates/op-host-services/src/web_credentials.rs" <<'EOF'