feat(ai): seed semantic palette at orchestration start
Port the TS applySemanticPalette 56-token palette (22 themed light/dark colors on a Mode axis, 6 chart singles, 28 numerics) and wake the dormant variables.rs seeding path. Merge semantics differ from TS: editor-core set_variables_bulk extends (incoming wins) while TS keeps existing values, so the snapshot computes the missing set first and seeds only absent tokens via one MergeThemePreset; rollback removes exactly the created names/axes when no content survives. Live-smoke verified: generated .op carries the 56 vars + Mode axis and $color-* refs resolve at paint.
This commit is contained in:
parent
9ffaf5980c
commit
a9b0e729fd
|
|
@ -270,6 +270,7 @@ fn cleanup_concurrent_all_scaffold_only_deletes_all_and_rolls_back_vars() {
|
|||
// no content survived.
|
||||
let snap = VarSnapshot {
|
||||
created: vec!["color-primary".into()],
|
||||
created_axes: vec![],
|
||||
};
|
||||
cleanup_concurrent_roots(&mut sink, &[&root_a, &root_b], &[0, 0], &snap);
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ pub mod plan;
|
|||
pub mod plan_normalize;
|
||||
pub mod plan_repair;
|
||||
pub mod retry;
|
||||
pub mod semantic_palette;
|
||||
pub mod stub_providers;
|
||||
pub mod style_guide_context;
|
||||
pub mod timeouts;
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ impl Orchestrator {
|
|||
let var_snapshot = snapshot_plan_vars(sink, &plan);
|
||||
|
||||
// -- 阶段 2:画布搭建 --
|
||||
for cmd in seed_commands(&plan) {
|
||||
for cmd in seed_commands(&plan, &var_snapshot) {
|
||||
sink.apply(cmd);
|
||||
}
|
||||
let scaffold_root_index = sink.state().active_children().len();
|
||||
|
|
@ -440,7 +440,7 @@ async fn run_concurrent_path(
|
|||
let var_snapshot = snapshot_plan_vars(sink, &plan);
|
||||
|
||||
// -- 阶段 2 (并发):变量播种 + N-root scaffold --
|
||||
for cmd in seed_commands(&plan) {
|
||||
for cmd in seed_commands(&plan, &var_snapshot) {
|
||||
sink.apply(cmd);
|
||||
}
|
||||
|
||||
|
|
|
|||
218
crates/op-orchestrator/src/semantic_palette.rs
Normal file
218
crates/op-orchestrator/src/semantic_palette.rs
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
//! 56-token semantic palette — the Rust port of
|
||||
//! `packages/pen-core/src/variables/semantic-palette.ts`.
|
||||
//!
|
||||
//! Composition (P1 token system, values verbatim from the TS table):
|
||||
//! - 14 base color tokens (surface, border, text, accent, scrim) — light/dark
|
||||
//! - 8 alert color tokens (info/success/warning/danger × bg/text) — light/dark
|
||||
//! - 6 chart color tokens (color-chart-1..6) — single-value
|
||||
//! - 18 typography tokens (size + weight + line-height × 6 roles) — numeric
|
||||
//! - 2 letterSpacing tokens — numeric
|
||||
//! - 5 spacing tokens (4/8/12/16/24) — numeric
|
||||
//! - 3 radius tokens (4/8/12) — numeric
|
||||
//!
|
||||
//! Theme-aware tokens carry BOTH values on the single `Mode: [Light,
|
||||
//! Dark]` axis; single-value entries have no theme axis. The palette is
|
||||
//! what `$color-*` / `$type-*` refs emitted by theme="system" element
|
||||
//! builders resolve against (paint-time resolution lives in
|
||||
//! `op-editor-ui::scene_vars::VariableTable`).
|
||||
|
||||
use jian_ops_schema::variable::{
|
||||
ThemedValue, VariableDefinition, VariableKind, VariableScalar, VariableValue,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// The single theme axis the palette declares.
|
||||
pub const THEME_AXIS: &str = "Mode";
|
||||
pub const THEME_LIGHT: &str = "Light";
|
||||
pub const THEME_DARK: &str = "Dark";
|
||||
|
||||
enum Entry {
|
||||
/// Theme-aware color — (light hex, dark hex).
|
||||
LightDark(&'static str, &'static str),
|
||||
/// Single-value color, no theme axis (chart palette).
|
||||
Color(&'static str),
|
||||
/// Numeric token, no theme axis (typography / spacing / radius).
|
||||
Num(f64),
|
||||
}
|
||||
|
||||
/// Raw value table — source of truth, kept in the same order as the TS
|
||||
/// `PALETTE` constant so review diffs line up.
|
||||
const PALETTE: &[(&str, Entry)] = &[
|
||||
("color-surface", Entry::LightDark("#FFFFFF", "#1E293B")),
|
||||
("color-surface-2", Entry::LightDark("#F1F5F9", "#334155")),
|
||||
("color-surface-3", Entry::LightDark("#F3F4F6", "#475569")),
|
||||
("color-bg-deep", Entry::LightDark("#F8FAFC", "#0F172A")),
|
||||
("color-border", Entry::LightDark("#E2E8F0", "#334155")),
|
||||
(
|
||||
"color-border-strong",
|
||||
Entry::LightDark("#CBD5E1", "#475569"),
|
||||
),
|
||||
("color-text-primary", Entry::LightDark("#0F172A", "#F1F5F9")),
|
||||
("color-text-body", Entry::LightDark("#334155", "#CBD5E1")),
|
||||
("color-text-muted", Entry::LightDark("#64748B", "#94A3B8")),
|
||||
("color-text-subtle", Entry::LightDark("#94A3B8", "#64748B")),
|
||||
("color-accent", Entry::LightDark("#2563EB", "#60A5FA")),
|
||||
("color-destructive", Entry::LightDark("#EF4444", "#F87171")),
|
||||
("color-success", Entry::LightDark("#10B981", "#34D399")),
|
||||
("color-scrim", Entry::LightDark("#00000080", "#00000099")),
|
||||
// ── Alert tokens ──────────────────────────────────────────────
|
||||
("color-info-bg", Entry::LightDark("#DBEAFE", "#1E3A8A")),
|
||||
("color-info-text", Entry::LightDark("#1E40AF", "#BFDBFE")),
|
||||
("color-success-bg", Entry::LightDark("#DCFCE7", "#14532D")),
|
||||
("color-success-text", Entry::LightDark("#166534", "#BBF7D0")),
|
||||
("color-warning-bg", Entry::LightDark("#FEF3C7", "#78350F")),
|
||||
("color-warning-text", Entry::LightDark("#92400E", "#FDE68A")),
|
||||
("color-danger-bg", Entry::LightDark("#FEE2E2", "#7F1D1D")),
|
||||
("color-danger-text", Entry::LightDark("#991B1B", "#FECACA")),
|
||||
// ── Typography: size + weight + line-height × 6 roles ─────────
|
||||
("type-display-size", Entry::Num(64.0)),
|
||||
("type-display-weight", Entry::Num(700.0)),
|
||||
("type-display-line-height", Entry::Num(1.0)),
|
||||
("type-h1-size", Entry::Num(24.0)),
|
||||
("type-h1-weight", Entry::Num(600.0)),
|
||||
("type-h1-line-height", Entry::Num(1.2)),
|
||||
("type-h2-size", Entry::Num(20.0)),
|
||||
("type-h2-weight", Entry::Num(600.0)),
|
||||
("type-h2-line-height", Entry::Num(1.25)),
|
||||
("type-h3-size", Entry::Num(16.0)),
|
||||
("type-h3-weight", Entry::Num(600.0)),
|
||||
("type-h3-line-height", Entry::Num(1.3)),
|
||||
("type-body-size", Entry::Num(14.0)),
|
||||
("type-body-weight", Entry::Num(400.0)),
|
||||
("type-body-line-height", Entry::Num(1.5)),
|
||||
("type-caption-size", Entry::Num(12.0)),
|
||||
("type-caption-weight", Entry::Num(400.0)),
|
||||
("type-caption-line-height", Entry::Num(1.4)),
|
||||
// ── Border radius ─────────────────────────────────────────────
|
||||
("radius-sm", Entry::Num(4.0)),
|
||||
("radius-md", Entry::Num(8.0)),
|
||||
("radius-lg", Entry::Num(12.0)),
|
||||
// ── Spacing scale ─────────────────────────────────────────────
|
||||
("spacing-1", Entry::Num(4.0)),
|
||||
("spacing-2", Entry::Num(8.0)),
|
||||
("spacing-3", Entry::Num(12.0)),
|
||||
("spacing-4", Entry::Num(16.0)),
|
||||
("spacing-5", Entry::Num(24.0)),
|
||||
// ── Sparse letterSpacing ──────────────────────────────────────
|
||||
("type-display-letter-spacing", Entry::Num(-0.5)),
|
||||
("type-uppercase-label-letter-spacing", Entry::Num(1.5)),
|
||||
// ── Chart palette ─────────────────────────────────────────────
|
||||
("color-chart-1", Entry::Color("#3B82F6")),
|
||||
("color-chart-2", Entry::Color("#8B5CF6")),
|
||||
("color-chart-3", Entry::Color("#EC4899")),
|
||||
("color-chart-4", Entry::Color("#14B8A6")),
|
||||
("color-chart-5", Entry::Color("#F59E0B")),
|
||||
("color-chart-6", Entry::Color("#F97316")),
|
||||
];
|
||||
|
||||
/// Every palette token name, table order.
|
||||
pub fn palette_names() -> Vec<&'static str> {
|
||||
PALETTE.iter().map(|(name, _)| *name).collect()
|
||||
}
|
||||
|
||||
fn themed(mode: &str, hex: &str) -> ThemedValue {
|
||||
ThemedValue {
|
||||
value: VariableScalar::Str(hex.to_string()),
|
||||
theme: Some(BTreeMap::from([(THEME_AXIS.to_string(), mode.to_string())])),
|
||||
}
|
||||
}
|
||||
|
||||
/// One token's `VariableDefinition`; `None` for unknown names.
|
||||
pub fn palette_variable(name: &str) -> Option<VariableDefinition> {
|
||||
let entry = PALETTE.iter().find(|(n, _)| *n == name).map(|(_, e)| e)?;
|
||||
Some(match entry {
|
||||
Entry::LightDark(light, dark) => VariableDefinition {
|
||||
kind: VariableKind::Color,
|
||||
value: VariableValue::Themed(vec![
|
||||
themed(THEME_LIGHT, light),
|
||||
themed(THEME_DARK, dark),
|
||||
]),
|
||||
},
|
||||
Entry::Color(hex) => VariableDefinition {
|
||||
kind: VariableKind::Color,
|
||||
value: VariableValue::Scalar(VariableScalar::Str(hex.to_string())),
|
||||
},
|
||||
Entry::Num(n) => VariableDefinition {
|
||||
kind: VariableKind::Number,
|
||||
value: VariableValue::Scalar(VariableScalar::Num(*n)),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// The full palette as a variables map (for `MergeThemePreset`).
|
||||
pub fn palette_variables() -> BTreeMap<String, VariableDefinition> {
|
||||
PALETTE
|
||||
.iter()
|
||||
.map(|(name, _)| {
|
||||
(
|
||||
name.to_string(),
|
||||
palette_variable(name).expect("table name"),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The `Mode: [Light, Dark]` theme axis declaration.
|
||||
pub fn palette_themes() -> BTreeMap<String, Vec<String>> {
|
||||
BTreeMap::from([(
|
||||
THEME_AXIS.to_string(),
|
||||
vec![THEME_LIGHT.to_string(), THEME_DARK.to_string()],
|
||||
)])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 56 tokens: 22 themed colors + 6 chart colors + 28 numerics —
|
||||
/// the TS `SEMANTIC_PALETTE_NAMES` inventory.
|
||||
#[test]
|
||||
fn palette_inventory_matches_ts() {
|
||||
let vars = palette_variables();
|
||||
assert_eq!(vars.len(), 56);
|
||||
let themed = vars
|
||||
.values()
|
||||
.filter(|v| matches!(v.value, VariableValue::Themed(_)))
|
||||
.count();
|
||||
let numbers = vars
|
||||
.values()
|
||||
.filter(|v| v.kind == VariableKind::Number)
|
||||
.count();
|
||||
assert_eq!(themed, 22, "14 base + 8 alert light/dark pairs");
|
||||
assert_eq!(numbers, 28, "18 type + 2 tracking + 5 spacing + 3 radius");
|
||||
assert_eq!(vars.len() - themed - numbers, 6, "chart singles");
|
||||
}
|
||||
|
||||
/// Spot-check values stay verbatim with the TS table.
|
||||
#[test]
|
||||
fn spot_values_verbatim() {
|
||||
let accent = palette_variable("color-accent").unwrap();
|
||||
let VariableValue::Themed(pair) = &accent.value else {
|
||||
panic!("accent must be themed");
|
||||
};
|
||||
assert_eq!(pair[0].value, VariableScalar::Str("#2563EB".into()));
|
||||
assert_eq!(pair[1].value, VariableScalar::Str("#60A5FA".into()));
|
||||
assert_eq!(
|
||||
pair[0].theme.as_ref().unwrap().get(THEME_AXIS),
|
||||
Some(&THEME_LIGHT.to_string())
|
||||
);
|
||||
|
||||
let body = palette_variable("type-body-size").unwrap();
|
||||
assert_eq!(body.value, VariableValue::Scalar(VariableScalar::Num(14.0)));
|
||||
|
||||
let chart = palette_variable("color-chart-1").unwrap();
|
||||
assert_eq!(
|
||||
chart.value,
|
||||
VariableValue::Scalar(VariableScalar::Str("#3B82F6".into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn themes_declare_mode_axis() {
|
||||
let themes = palette_themes();
|
||||
assert_eq!(
|
||||
themes.get(THEME_AXIS),
|
||||
Some(&vec!["Light".to_string(), "Dark".to_string()])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +1,107 @@
|
|||
//! plan 派生变量 —— seed / 快照 / 回滚。
|
||||
//! 设计变量 seed —— 语义调色板播种 / 快照 / 回滚。
|
||||
//!
|
||||
//! S3b-1a:plan 已无 `palette`。三个公共函数入眠态(签名不变,
|
||||
//! `run.rs` 调用方不动)。忠实的 styleGuideName → 解析 guide →
|
||||
//! 播种变量是后续项。
|
||||
//! 编排开始时把 56-token 语义调色板([`crate::semantic_palette`],
|
||||
//! TS `applySemanticPalette` 的 Rust 移植)播进文档,让 theme="system"
|
||||
//! 元素发出的 `$color-*` 引用在 paint 时能解析(此前 seeding 休眠,
|
||||
//! 悬空引用渲染灰底)。
|
||||
//!
|
||||
//! 合并语义:**已有者赢**(对齐 TS)。`set_variables_bulk(merge)` 在
|
||||
//! editor-core 侧是 `.extend`(新值赢),所以这里先用快照算出缺失集,
|
||||
//! seed 只发缺失的 token / 轴 —— 文档里已自定义的 `color-accent`
|
||||
//! 永远不被覆盖。
|
||||
//!
|
||||
//! 忠实的 styleGuideName → 解析 guide → 播种 guide 调色板仍是后续项;
|
||||
//! 本模块是其缺省底座(DEFAULT_PALETTE 等价物)。
|
||||
|
||||
use crate::plan::OrchestratorPlan;
|
||||
use crate::semantic_palette;
|
||||
use crate::types::DocSink;
|
||||
use op_editor_core::EditorCommand;
|
||||
|
||||
/// 回滚快照 —— seed 前"不存在"的 plan 变量名集合(即 seed 会
|
||||
/// 真正新建的那批)。
|
||||
/// 回滚快照 —— seed 前"不存在"的调色板变量名 / 主题轴集合(即 seed
|
||||
/// 会真正新建的那批)。
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct VarSnapshot {
|
||||
/// seed 前不存在的变量名 —— 回滚时删除这些。
|
||||
pub created: Vec<String>,
|
||||
/// seed 前不存在的主题轴 —— 回滚时从 themes 里剔除。
|
||||
pub created_axes: Vec<String>,
|
||||
}
|
||||
|
||||
/// 在 seed *之前* 调用。S3b-1a:plan 已无 `palette`,恒为空快照。
|
||||
/// 忠实的 styleGuideName → 解析 guide → 播种变量是后续项。
|
||||
pub fn snapshot_plan_vars(_sink: &dyn DocSink, _plan: &OrchestratorPlan) -> VarSnapshot {
|
||||
VarSnapshot::default()
|
||||
/// 在 seed *之前* 调用:对照现有文档变量/主题轴,算出调色板里缺失
|
||||
/// 的部分。快照同时是 [`seed_commands`] 的输入(只播缺失项)和
|
||||
/// [`rollback`] 的依据(只删自己新建的)。
|
||||
pub fn snapshot_plan_vars(sink: &dyn DocSink, _plan: &OrchestratorPlan) -> VarSnapshot {
|
||||
let doc = &sink.state().doc;
|
||||
let existing_vars = doc.variables.as_ref();
|
||||
let created = semantic_palette::palette_names()
|
||||
.into_iter()
|
||||
.filter(|name| !existing_vars.is_some_and(|vars| vars.contains_key(*name)))
|
||||
.map(str::to_string)
|
||||
.collect();
|
||||
let existing_axes = doc.themes.as_ref();
|
||||
let created_axes = semantic_palette::palette_themes()
|
||||
.into_keys()
|
||||
.filter(|axis| !existing_axes.is_some_and(|axes| axes.contains_key(axis)))
|
||||
.collect();
|
||||
VarSnapshot {
|
||||
created,
|
||||
created_axes,
|
||||
}
|
||||
}
|
||||
|
||||
/// plan 调色板 → seed 命令。S3b-1a:plan 无 `palette`,恒为空。
|
||||
pub fn seed_commands(_plan: &OrchestratorPlan) -> Vec<EditorCommand> {
|
||||
Vec::new()
|
||||
/// 缺失的调色板 token / 轴 → 一条 `MergeThemePreset`。全部已存在时
|
||||
/// 返回空(完全播种过的文档零命令)。
|
||||
pub fn seed_commands(_plan: &OrchestratorPlan, snap: &VarSnapshot) -> Vec<EditorCommand> {
|
||||
if snap.created.is_empty() && snap.created_axes.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let palette = semantic_palette::palette_variables();
|
||||
let variables = snap
|
||||
.created
|
||||
.iter()
|
||||
.filter_map(|name| palette.get(name).map(|def| (name.clone(), def.clone())))
|
||||
.collect();
|
||||
let themes = semantic_palette::palette_themes()
|
||||
.into_iter()
|
||||
.filter(|(axis, _)| snap.created_axes.contains(axis))
|
||||
.collect();
|
||||
vec![EditorCommand::MergeThemePreset { variables, themes }]
|
||||
}
|
||||
|
||||
/// 回滚 seed 新建的变量。S3b-1a:无 seed,故 no-op(快照恒空)。
|
||||
/// 回滚 seed 新建的变量与主题轴(已有者从未被覆盖,所以恢复 = 删除
|
||||
/// 新建项)。
|
||||
pub fn rollback(sink: &mut dyn DocSink, snap: &VarSnapshot) {
|
||||
for name in &snap.created {
|
||||
sink.apply(EditorCommand::DeleteVariable { name: name.clone() });
|
||||
}
|
||||
if !snap.created_axes.is_empty() {
|
||||
let themes = sink
|
||||
.state()
|
||||
.doc
|
||||
.themes
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|(axis, _)| !snap.created_axes.contains(axis))
|
||||
.collect();
|
||||
sink.apply(EditorCommand::SetThemes {
|
||||
themes,
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_support::VecDocSink;
|
||||
use jian_ops_schema::variable::{
|
||||
VariableDefinition, VariableKind, VariableScalar, VariableValue,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn seed_is_dormant() {
|
||||
let plan = crate::plan::build_fallback_plan(&crate::types::DesignRequest {
|
||||
fn plan() -> OrchestratorPlan {
|
||||
crate::plan::build_fallback_plan(&crate::types::DesignRequest {
|
||||
prompt: "a page".into(),
|
||||
model: None,
|
||||
provider: None,
|
||||
|
|
@ -51,10 +111,92 @@ mod tests {
|
|||
validation_enabled: true,
|
||||
|
||||
visual_ref_enabled: false,
|
||||
});
|
||||
assert!(seed_commands(&plan).is_empty());
|
||||
})
|
||||
}
|
||||
|
||||
/// 空文档:快照报告全部 56 token + Mode 轴缺失,seed 出一条
|
||||
/// MergeThemePreset 把它们全部带上。
|
||||
#[test]
|
||||
fn seed_on_empty_doc_carries_full_palette() {
|
||||
let sink = VecDocSink::new();
|
||||
assert!(snapshot_plan_vars(&sink, &plan).created.is_empty());
|
||||
let snap = snapshot_plan_vars(&sink, &plan());
|
||||
assert_eq!(snap.created.len(), 56);
|
||||
assert_eq!(snap.created_axes, vec!["Mode".to_string()]);
|
||||
|
||||
let cmds = seed_commands(&plan(), &snap);
|
||||
assert_eq!(cmds.len(), 1);
|
||||
let EditorCommand::MergeThemePreset { variables, themes } = &cmds[0] else {
|
||||
panic!("expected MergeThemePreset, got {:?}", cmds[0]);
|
||||
};
|
||||
assert_eq!(variables.len(), 56);
|
||||
assert!(variables.contains_key("color-accent"));
|
||||
assert_eq!(themes.get("Mode").map(Vec::len), Some(2));
|
||||
}
|
||||
|
||||
/// 已有同名变量获胜(TS applySemanticPalette 语义):快照不把它
|
||||
/// 计入 created,seed 不发它 —— editor-core 的 merge 是新值赢,
|
||||
/// 守住"已有者赢"全靠这里不发。
|
||||
#[test]
|
||||
fn seed_respects_existing_variables_and_axes() {
|
||||
let mut sink = VecDocSink::new();
|
||||
sink.state.doc.variables = Some(
|
||||
[(
|
||||
"color-accent".to_string(),
|
||||
VariableDefinition {
|
||||
kind: VariableKind::Color,
|
||||
value: VariableValue::Scalar(VariableScalar::Str("#FF00FF".into())),
|
||||
},
|
||||
)]
|
||||
.into(),
|
||||
);
|
||||
sink.state.doc.themes = Some([("Mode".to_string(), vec!["Light".to_string()])].into());
|
||||
|
||||
let snap = snapshot_plan_vars(&sink, &plan());
|
||||
assert_eq!(snap.created.len(), 55);
|
||||
assert!(!snap.created.contains(&"color-accent".to_string()));
|
||||
assert!(snap.created_axes.is_empty(), "Mode axis pre-exists");
|
||||
|
||||
let cmds = seed_commands(&plan(), &snap);
|
||||
let EditorCommand::MergeThemePreset { variables, themes } = &cmds[0] else {
|
||||
panic!("expected MergeThemePreset");
|
||||
};
|
||||
assert!(!variables.contains_key("color-accent"));
|
||||
assert!(themes.is_empty());
|
||||
}
|
||||
|
||||
/// 全量播种过的文档:零命令。
|
||||
#[test]
|
||||
fn seed_is_noop_when_fully_seeded() {
|
||||
let mut sink = VecDocSink::new();
|
||||
sink.state.doc.variables = Some(
|
||||
crate::semantic_palette::palette_variables()
|
||||
.into_iter()
|
||||
.collect(),
|
||||
);
|
||||
sink.state.doc.themes = Some(crate::semantic_palette::palette_themes());
|
||||
let snap = snapshot_plan_vars(&sink, &plan());
|
||||
assert!(snap.created.is_empty());
|
||||
assert!(snap.created_axes.is_empty());
|
||||
assert!(seed_commands(&plan(), &snap).is_empty());
|
||||
}
|
||||
|
||||
/// 回滚删除新建变量,并把新建的 Mode 轴从 themes 里剔除。
|
||||
#[test]
|
||||
fn rollback_deletes_created_vars_and_axes() {
|
||||
let mut sink = VecDocSink::new();
|
||||
let snap = VarSnapshot {
|
||||
created: vec!["color-accent".to_string()],
|
||||
created_axes: vec!["Mode".to_string()],
|
||||
};
|
||||
rollback(&mut sink, &snap);
|
||||
assert!(sink.applied.iter().any(|c| matches!(
|
||||
c,
|
||||
EditorCommand::DeleteVariable { name } if name == "color-accent"
|
||||
)));
|
||||
assert!(sink.applied.iter().any(|c| matches!(
|
||||
c,
|
||||
EditorCommand::SetThemes { themes, replace: true } if !themes.contains_key("Mode")
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue