feat(orchestrator): types — AppendContext + append_context + existing_section_labels
This commit is contained in:
parent
3484bd3783
commit
25b3b8bf39
|
|
@ -34,6 +34,7 @@ fn subtask_with_screen(id: &str, screen: Option<&str>) -> Subtask {
|
|||
elements: None,
|
||||
screen: screen.map(|s| s.to_string()),
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -64,6 +65,7 @@ fn make_plan_with_subtasks(subtask_ids: &[&str]) -> crate::plan::OrchestratorPla
|
|||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
})
|
||||
.collect(),
|
||||
style_guide_name: None,
|
||||
|
|
@ -77,6 +79,7 @@ fn make_req() -> crate::types::DesignRequest {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 2,
|
||||
append_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ fn make_req() -> crate::types::DesignRequest {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 2,
|
||||
append_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,6 +52,7 @@ fn make_two_screen_plan() -> crate::plan::OrchestratorPlan {
|
|||
elements: None,
|
||||
screen: Some("login".into()),
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
},
|
||||
crate::plan::Subtask {
|
||||
id: "s1".into(),
|
||||
|
|
@ -64,6 +66,7 @@ fn make_two_screen_plan() -> crate::plan::OrchestratorPlan {
|
|||
elements: None,
|
||||
screen: Some("home".into()),
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
},
|
||||
],
|
||||
style_guide_name: None,
|
||||
|
|
@ -86,6 +89,7 @@ fn make_three_screen_plan() -> crate::plan::OrchestratorPlan {
|
|||
elements: None,
|
||||
screen: Some(screen.into()),
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
};
|
||||
OrchestratorPlan {
|
||||
root_frame: RootFrameSpec {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ fn subtask(id: &str, label: &str, elements: Option<&str>) -> Subtask {
|
|||
elements: elements.map(String::from),
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ fn subtask(id: &str, label: &str, elements: Option<&str>) -> Subtask {
|
|||
elements: elements.map(String::from),
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ fn subtask(id: &str, label: &str, elements: Option<&str>) -> Subtask {
|
|||
elements: elements.map(String::from),
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ fn st(id: &str, label: &str, width: f64, height: f64) -> Subtask {
|
|||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -49,6 +50,7 @@ fn sidebar_st(height: f64) -> Subtask {
|
|||
elements: Some("nav links".into()),
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@
|
|||
//! `buildFallbackPlanFromPrompt`)。
|
||||
|
||||
use crate::types::DesignRequest;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 根 frame 的一个 fill —— 对齐 TS canonical `[{type,color}]`。
|
||||
/// 规划只需 solid 色;非 solid 项保留 `color` 字段(可能为空)。
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PlanFill {
|
||||
#[serde(rename = "type", default)]
|
||||
pub kind: String,
|
||||
|
|
@ -18,14 +18,14 @@ pub struct PlanFill {
|
|||
}
|
||||
|
||||
/// 一个 subtask 区域的尺寸。
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Region {
|
||||
pub width: f64,
|
||||
pub height: f64,
|
||||
}
|
||||
|
||||
/// 根 frame 规格。字段对齐规划语料 `decomposition.md` 的 `rootFrame`。
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RootFrameSpec {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
|
|
@ -54,7 +54,8 @@ impl RootFrameSpec {
|
|||
}
|
||||
|
||||
/// 一个生成子任务 —— 对应设计里的一个区块。
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Subtask {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
|
|
@ -75,10 +76,14 @@ pub struct Subtask {
|
|||
/// `SubTask.generatedRootId`。仪器由 run.rs 填写;规划阶段为 None。
|
||||
#[serde(skip)]
|
||||
pub generated_root_id: Option<String>,
|
||||
/// 从 AppendContext 传播而来 —— 告知 sub-agent 哪些已有区块不要重复。
|
||||
/// port of TS `SubTask.existingSectionLabels` (`ai-types.ts:134`)。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub existing_section_labels: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// 规划阶段的完整产物。字段对齐规划语料 `decomposition.md`。
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OrchestratorPlan {
|
||||
#[serde(rename = "rootFrame")]
|
||||
pub root_frame: RootFrameSpec,
|
||||
|
|
@ -177,6 +182,7 @@ pub fn build_fallback_plan(req: &DesignRequest) -> OrchestratorPlan {
|
|||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -211,6 +217,7 @@ mod tests {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -265,4 +272,72 @@ mod tests {
|
|||
assert_eq!(long.subtasks.len(), 3);
|
||||
assert!(!long.subtasks.is_empty());
|
||||
}
|
||||
|
||||
// ── Task A1: Subtask.existing_section_labels ──────────────────────────────
|
||||
|
||||
/// Subtask accepts existing_section_labels: None without breaking compilation.
|
||||
#[test]
|
||||
fn subtask_existing_section_labels_none_compiles() {
|
||||
let st = Subtask {
|
||||
id: "hero".into(),
|
||||
label: "Hero".into(),
|
||||
region: Region {
|
||||
width: 1200.0,
|
||||
height: 400.0,
|
||||
},
|
||||
id_prefix: "hero".into(),
|
||||
parent_frame_id: None,
|
||||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
};
|
||||
assert!(st.existing_section_labels.is_none());
|
||||
}
|
||||
|
||||
/// Subtask accepts a populated existing_section_labels vec.
|
||||
#[test]
|
||||
fn subtask_existing_section_labels_some_compiles() {
|
||||
let st = Subtask {
|
||||
id: "features".into(),
|
||||
label: "Features".into(),
|
||||
region: Region {
|
||||
width: 1200.0,
|
||||
height: 400.0,
|
||||
},
|
||||
id_prefix: "features".into(),
|
||||
parent_frame_id: None,
|
||||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: Some(vec!["Hero".into(), "About".into()]),
|
||||
};
|
||||
let labels = st.existing_section_labels.as_ref().unwrap();
|
||||
assert_eq!(labels[0], "Hero");
|
||||
assert_eq!(labels[1], "About");
|
||||
}
|
||||
|
||||
/// Subtask with existing_section_labels = Some([]) serializes without the field.
|
||||
#[test]
|
||||
fn subtask_existing_section_labels_omitted_when_none() {
|
||||
let st = Subtask {
|
||||
id: "hero".into(),
|
||||
label: "Hero".into(),
|
||||
region: Region {
|
||||
width: 1200.0,
|
||||
height: 400.0,
|
||||
},
|
||||
id_prefix: "hero".into(),
|
||||
parent_frame_id: None,
|
||||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
};
|
||||
let json = serde_json::to_string(&st).expect("serialize");
|
||||
assert!(
|
||||
!json.contains("existingSectionLabels"),
|
||||
"existingSectionLabels should be omitted when None, got: {json}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ mod tests {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,6 +99,7 @@ mod tests {
|
|||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +174,7 @@ mod tests {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -189,6 +192,7 @@ mod tests {
|
|||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
};
|
||||
// chart subtask — LLM-provided height 300, within [inferred*0.6, inferred*1.6]
|
||||
// inferred for "chart" = 320 → min=192, max=512 → 300 in range → keep 300
|
||||
|
|
@ -204,6 +208,7 @@ mod tests {
|
|||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
};
|
||||
// metric subtask — LLM-provided height 0 (invalid) → use inferred = 160
|
||||
let st_metric = Subtask {
|
||||
|
|
@ -218,6 +223,7 @@ mod tests {
|
|||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
};
|
||||
OrchestratorPlan {
|
||||
root_frame: RootFrameSpec {
|
||||
|
|
@ -317,6 +323,7 @@ mod tests {
|
|||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
}],
|
||||
style_guide_name: None,
|
||||
};
|
||||
|
|
@ -356,6 +363,7 @@ mod tests {
|
|||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
}],
|
||||
style_guide_name: None,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -359,6 +359,7 @@ fn coerce_subtask(
|
|||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -410,6 +411,7 @@ fn coerce_subtask(
|
|||
elements,
|
||||
screen,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -243,6 +243,7 @@ fn req(prompt: &str) -> DesignRequest {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -266,6 +267,7 @@ fn req_with_design_md(prompt: &str) -> DesignRequest {
|
|||
generation_notes: None,
|
||||
}),
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -230,6 +230,7 @@ mod tests {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -310,6 +311,7 @@ mod tests {
|
|||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
};
|
||||
let cr = build_subagent_prompt(&st, &plan(), &req(), AbortFlag::new(), false, false);
|
||||
assert!(cr.user_prompt.contains("Hero"));
|
||||
|
|
@ -331,6 +333,7 @@ mod tests {
|
|||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
};
|
||||
// minimal_skills=true: the system prompt should contain "schema" skill
|
||||
// content and "jsonl-format" skill content, but NOT layout/text-rules etc.
|
||||
|
|
@ -362,6 +365,7 @@ mod tests {
|
|||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
};
|
||||
// req() uses model "claude" which is Full tier — no narrowing.
|
||||
// Use a basic-tier model to test narrowing.
|
||||
|
|
@ -371,6 +375,7 @@ mod tests {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
};
|
||||
let full_cr =
|
||||
build_subagent_prompt(&st, &plan(), &basic_req, AbortFlag::new(), false, false);
|
||||
|
|
@ -396,6 +401,7 @@ mod tests {
|
|||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
};
|
||||
// req() uses "claude" which maps to Full tier → reduced_complexity is no-op
|
||||
let full_cr = build_subagent_prompt(&st, &plan(), &req(), AbortFlag::new(), false, false);
|
||||
|
|
@ -458,6 +464,7 @@ mod tests {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
};
|
||||
let long_prompt = "x".repeat(5000); // >= 4200 chars
|
||||
let long_req = DesignRequest {
|
||||
|
|
@ -466,6 +473,7 @@ mod tests {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
};
|
||||
let short_pp = build_orchestrator_prompt(&short_req, PlanningMode::Rich, AbortFlag::new());
|
||||
let long_pp = build_orchestrator_prompt(&long_req, PlanningMode::Rich, AbortFlag::new());
|
||||
|
|
@ -485,6 +493,7 @@ mod tests {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
};
|
||||
let pp = build_orchestrator_prompt(&ds_req, PlanningMode::Rich, AbortFlag::new());
|
||||
// Short bucket base: 300_000ms × 2.0 = 600_000ms
|
||||
|
|
@ -508,6 +517,7 @@ mod tests {
|
|||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -540,6 +550,7 @@ mod tests {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
};
|
||||
let long_req = DesignRequest {
|
||||
prompt: "x".repeat(5000),
|
||||
|
|
@ -547,6 +558,7 @@ mod tests {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
};
|
||||
let short_cr = build_subagent_prompt(
|
||||
&subtask(),
|
||||
|
|
@ -579,6 +591,7 @@ mod tests {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
};
|
||||
let cr = build_subagent_prompt(
|
||||
&subtask(),
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ fn req() -> DesignRequest {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -25,6 +26,7 @@ fn req_standard() -> DesignRequest {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -37,6 +39,7 @@ fn req_basic() -> DesignRequest {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ fn req_concurrent() -> DesignRequest {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 2,
|
||||
append_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,6 +70,7 @@ fn req_sequential() -> DesignRequest {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ fn req_dashboard() -> DesignRequest {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +67,7 @@ fn req_non_dashboard() -> DesignRequest {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -304,6 +304,7 @@ mod tests {
|
|||
elements: None,
|
||||
screen: screen.map(|s| s.to_string()),
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ fn dashboard_plan(root_id: &str) -> OrchestratorPlan {
|
|||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
},
|
||||
Subtask {
|
||||
id: "revenue-chart".into(),
|
||||
|
|
@ -73,6 +74,7 @@ fn dashboard_plan(root_id: &str) -> OrchestratorPlan {
|
|||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
},
|
||||
Subtask {
|
||||
id: "kpi-metrics".into(),
|
||||
|
|
@ -86,6 +88,7 @@ fn dashboard_plan(root_id: &str) -> OrchestratorPlan {
|
|||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
},
|
||||
],
|
||||
style_guide_name: None,
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ mod tests {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -138,6 +139,7 @@ mod tests {
|
|||
elements: None,
|
||||
screen: None,
|
||||
generated_root_id: None,
|
||||
existing_section_labels: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
use futures::stream::BoxStream;
|
||||
use op_editor_core::{EditorCommand, EditorState};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
|
@ -164,8 +165,26 @@ impl std::fmt::Display for OrchestratorError {
|
|||
|
||||
impl std::error::Error for OrchestratorError {}
|
||||
|
||||
/// 追加上下文 —— 当用户要求扩展已有页面时由 host 填入。
|
||||
///
|
||||
/// Port of `AppendContext` in `apps/web/src/services/ai/ai-types.ts:28-37`.
|
||||
/// 当存在时,编排器跳过创建新根 frame,将生成的区块插入现有目标 frame。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppendContext {
|
||||
/// 新 sub-agent 区块应插入的 frame id。
|
||||
pub target_parent_id: String,
|
||||
/// 目标 frame 的宽度(用于给 sub-agent 确定区域尺寸)。
|
||||
pub target_width: f64,
|
||||
/// 现有顶层区块的标签列表 —— sub-agent 被告知不要重复这些。
|
||||
pub existing_section_labels: Vec<String>,
|
||||
/// 目标 frame 属于移动页面(宽度 ≤ 480)时为 true。
|
||||
pub is_mobile: bool,
|
||||
}
|
||||
|
||||
/// 编排器输入 —— 一次设计请求。
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DesignRequest {
|
||||
pub prompt: String,
|
||||
pub model: Option<String>,
|
||||
|
|
@ -176,6 +195,10 @@ pub struct DesignRequest {
|
|||
/// 调用方应传 store-clamped 值 [1,6];crate 内部防御性 clamp。
|
||||
/// 默认为 1(顺序执行)。Port of TS `request.concurrency ?? 1`.
|
||||
pub concurrency: u32,
|
||||
/// 追加模式上下文 —— 仅当 host 检测到 append intent 时填入。
|
||||
/// Port of `AIDesignRequest.context.appendContext` in `ai-types.ts:51`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub append_context: Option<AppendContext>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -201,4 +224,89 @@ mod tests {
|
|||
flag.set();
|
||||
assert!(clone.is_set());
|
||||
}
|
||||
|
||||
// ── Task A1: AppendContext + DesignRequest.append_context ─────────────────
|
||||
|
||||
/// AppendContext serde round-trips with all 4 fields (camelCase wire names).
|
||||
#[test]
|
||||
fn append_context_serde_round_trip() {
|
||||
let ctx = AppendContext {
|
||||
target_parent_id: "frame-abc".into(),
|
||||
target_width: 390.0,
|
||||
existing_section_labels: vec!["Hero".into(), "Pricing".into()],
|
||||
is_mobile: true,
|
||||
};
|
||||
let json = serde_json::to_string(&ctx).expect("serialize");
|
||||
// Wire names are camelCase
|
||||
assert!(
|
||||
json.contains("targetParentId"),
|
||||
"expected targetParentId in {json}"
|
||||
);
|
||||
assert!(
|
||||
json.contains("targetWidth"),
|
||||
"expected targetWidth in {json}"
|
||||
);
|
||||
assert!(
|
||||
json.contains("existingSectionLabels"),
|
||||
"expected existingSectionLabels in {json}"
|
||||
);
|
||||
assert!(json.contains("isMobile"), "expected isMobile in {json}");
|
||||
let back: AppendContext = serde_json::from_str(&json).expect("deserialize");
|
||||
assert_eq!(back.target_parent_id, "frame-abc");
|
||||
assert_eq!(back.target_width, 390.0);
|
||||
assert_eq!(back.existing_section_labels, vec!["Hero", "Pricing"]);
|
||||
assert!(back.is_mobile);
|
||||
}
|
||||
|
||||
/// DesignRequest accepts append_context: None without breaking compilation.
|
||||
#[test]
|
||||
fn design_request_append_context_none_compiles() {
|
||||
let req = DesignRequest {
|
||||
prompt: "test".into(),
|
||||
model: None,
|
||||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
};
|
||||
assert!(req.append_context.is_none());
|
||||
}
|
||||
|
||||
/// DesignRequest accepts a populated AppendContext.
|
||||
#[test]
|
||||
fn design_request_append_context_some_compiles() {
|
||||
let ctx = AppendContext {
|
||||
target_parent_id: "p1".into(),
|
||||
target_width: 1200.0,
|
||||
existing_section_labels: vec![],
|
||||
is_mobile: false,
|
||||
};
|
||||
let req = DesignRequest {
|
||||
prompt: "extend page".into(),
|
||||
model: None,
|
||||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: Some(ctx),
|
||||
};
|
||||
assert!(req.append_context.is_some());
|
||||
}
|
||||
|
||||
/// DesignRequest without append_context serializes without the field.
|
||||
#[test]
|
||||
fn design_request_append_context_omitted_from_json_when_none() {
|
||||
let req = DesignRequest {
|
||||
prompt: "test".into(),
|
||||
model: None,
|
||||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
};
|
||||
let json = serde_json::to_string(&req).expect("serialize");
|
||||
assert!(
|
||||
!json.contains("appendContext"),
|
||||
"appendContext should be omitted when None, got: {json}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ mod tests {
|
|||
provider: None,
|
||||
design_md: None,
|
||||
concurrency: 1,
|
||||
append_context: None,
|
||||
});
|
||||
assert!(seed_commands(&plan).is_empty());
|
||||
let sink = VecDocSink::new();
|
||||
|
|
|
|||
Loading…
Reference in a new issue