mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-05 04:07:36 +08:00
refactor: rebuild desktop app with Tauri
This commit is contained in:
@@ -0,0 +1,475 @@
|
||||
#[path = "support/fake_provider.rs"]
|
||||
mod fake_provider;
|
||||
#[path = "support/fixtures.rs"]
|
||||
mod fixtures;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use cursor_server::{
|
||||
cursor::{
|
||||
connect,
|
||||
prompting::{PromptAssets, PromptCompiler},
|
||||
proto::agent::v1 as pb,
|
||||
CursorCommand, CursorSessionRegistry,
|
||||
},
|
||||
model::{ContentPart, ProjectedContent},
|
||||
provider::{FinishReason, ModelEvent},
|
||||
store::{BlobId, Store},
|
||||
};
|
||||
use prost::Message;
|
||||
|
||||
#[tokio::test]
|
||||
async fn current_mode_and_referenced_context_are_consumed_by_one_runtime_message() {
|
||||
let (_directory, store) = fixtures::temp_store().await;
|
||||
let references = references(&store).await;
|
||||
let provider = fake_provider::FakeProvider::default();
|
||||
provider.push(vec![
|
||||
ModelEvent::Start {
|
||||
model_call_id: "model".into(),
|
||||
},
|
||||
ModelEvent::TextStart,
|
||||
ModelEvent::TextDelta("answer".into()),
|
||||
ModelEvent::TextEnd,
|
||||
ModelEvent::Done(FinishReason::Stop),
|
||||
]);
|
||||
let assets = PromptAssets::load(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("prompt/cursor")
|
||||
.as_path(),
|
||||
)
|
||||
.unwrap();
|
||||
let registry = CursorSessionRegistry::new(
|
||||
store,
|
||||
Arc::new(provider.clone()),
|
||||
PromptCompiler::new(assets),
|
||||
Default::default(),
|
||||
);
|
||||
let handle = registry.get_or_create("ask-request").await.unwrap();
|
||||
let mut output = handle.subscribe();
|
||||
handle
|
||||
.command(CursorCommand::Append {
|
||||
seqno: 0,
|
||||
message: Box::new(run_request(references)),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut seqno = 1;
|
||||
loop {
|
||||
let frame = tokio::time::timeout(std::time::Duration::from_secs(5), output.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let (flags, payload) = connect::decode_frames(&frame).unwrap().pop().unwrap();
|
||||
if flags & connect::END_STREAM_FLAG != 0 {
|
||||
break;
|
||||
}
|
||||
let message = pb::AgentServerMessage::decode(payload).unwrap();
|
||||
if let Some(pb::agent_server_message::Message::KvServerMessage(kv)) = message.message {
|
||||
handle
|
||||
.command(CursorCommand::Append {
|
||||
seqno,
|
||||
message: Box::new(kv_ack(kv.id)),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
seqno += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let requests = provider.requests();
|
||||
let request = &requests[0];
|
||||
assert!(request
|
||||
.prompt
|
||||
.tools
|
||||
.iter()
|
||||
.any(|tool| tool.name == "AskQuestion"));
|
||||
assert!(!request
|
||||
.prompt
|
||||
.tools
|
||||
.iter()
|
||||
.any(|tool| tool.name == "GenerateImage"));
|
||||
assert_eq!(request.history.len(), 1);
|
||||
assert_eq!(
|
||||
request.history[0].message_id,
|
||||
"runtime:run-request:ask-request"
|
||||
);
|
||||
let ProjectedContent::Parts(parts) = &request.history[0].content else {
|
||||
panic!("runtime message must use typed parts")
|
||||
};
|
||||
let [ContentPart::Text { text }] = parts.as_slice() else {
|
||||
panic!("this fixture has no images")
|
||||
};
|
||||
for expected in [
|
||||
"<user_rule>\nworkspace rule\n</user_rule>",
|
||||
"<agent_skill fullPath=\"/skills/test/SKILL.md\">test skill</agent_skill>",
|
||||
"<subagent name=\"reviewer\">review code</subagent>",
|
||||
"<mcp_meta_tool_server name=\"test\" identifier=\"mcp-test\">",
|
||||
"<mcp_tool name=\"lookup\">",
|
||||
"<definition_path>/tmp/mcp-test/lookup.json</definition_path>",
|
||||
"<input_schema>{"properties":{"query":{"type":"string"}},"type":"object"}</input_schema>",
|
||||
"Call a listed tool directly with CallMcpTool without calling GetMcpTools first.",
|
||||
"Ask mode is active.",
|
||||
"<user_query>\nexplain this\n</user_query>",
|
||||
] {
|
||||
assert!(
|
||||
text.contains(expected),
|
||||
"missing runtime section: {expected}"
|
||||
);
|
||||
}
|
||||
assert!(!text.contains("complete skill body"));
|
||||
assert!(!text.contains("complete MCP server instructions"));
|
||||
assert!(text.contains("/workspace/src/main.rs"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_context_parts_use_current_cursor_response_and_cache_its_content() {
|
||||
let (_directory, store) = fixtures::temp_store().await;
|
||||
let referenced_context = fixture_context();
|
||||
let references = references_for(&referenced_context);
|
||||
let mut current_context = referenced_context.clone();
|
||||
current_context
|
||||
.mcp_meta_tool_options
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.mcp_descriptors
|
||||
.push(pb::McpDescriptor {
|
||||
server_identifier: "live-mcp".into(),
|
||||
tools: vec![pb::McpToolDescriptor {
|
||||
tool_name: "current-tool".into(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
let provider = fake_provider::FakeProvider::default();
|
||||
provider.push(vec![
|
||||
ModelEvent::Start {
|
||||
model_call_id: "model".into(),
|
||||
},
|
||||
ModelEvent::TextStart,
|
||||
ModelEvent::TextDelta("answer".into()),
|
||||
ModelEvent::TextEnd,
|
||||
ModelEvent::Done(FinishReason::Stop),
|
||||
]);
|
||||
let assets = PromptAssets::load(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("prompt/cursor")
|
||||
.as_path(),
|
||||
)
|
||||
.unwrap();
|
||||
let registry = CursorSessionRegistry::new(
|
||||
store.clone(),
|
||||
Arc::new(provider.clone()),
|
||||
PromptCompiler::new(assets),
|
||||
Default::default(),
|
||||
);
|
||||
let handle = registry.get_or_create("context-request").await.unwrap();
|
||||
let mut output = handle.subscribe();
|
||||
handle
|
||||
.command(CursorCommand::Append {
|
||||
seqno: 0,
|
||||
message: Box::new(run_request(references)),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut seqno = 1;
|
||||
let mut requested_context = false;
|
||||
loop {
|
||||
let frame = tokio::time::timeout(std::time::Duration::from_secs(5), output.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let (flags, payload) = connect::decode_frames(&frame).unwrap().pop().unwrap();
|
||||
if flags & connect::END_STREAM_FLAG != 0 {
|
||||
break;
|
||||
}
|
||||
let message = pb::AgentServerMessage::decode(payload).unwrap();
|
||||
match message.message {
|
||||
Some(pb::agent_server_message::Message::ExecServerMessage(exec)) => {
|
||||
assert_eq!(exec.id, 0);
|
||||
let Some(pb::exec_server_message::Message::RequestContextArgs(args)) = exec.message
|
||||
else {
|
||||
panic!("missing context must use RequestContextArgs")
|
||||
};
|
||||
assert_eq!(args.notes_session_id.as_deref(), Some("mode-conversation"));
|
||||
requested_context = true;
|
||||
handle
|
||||
.command(CursorCommand::Append {
|
||||
seqno,
|
||||
message: Box::new(context_stream_close()),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
seqno += 1;
|
||||
handle
|
||||
.command(CursorCommand::Append {
|
||||
seqno,
|
||||
message: Box::new(pb::AgentClientMessage {
|
||||
message: Some(pb::agent_client_message::Message::ExecClientMessage(
|
||||
pb::ExecClientMessage {
|
||||
id: 0,
|
||||
message: Some(
|
||||
pb::exec_client_message::Message::RequestContextResult(
|
||||
pb::RequestContextResult {
|
||||
result: Some(
|
||||
pb::request_context_result::Result::Success(
|
||||
pb::RequestContextSuccess {
|
||||
request_context: Some(
|
||||
current_context.clone(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
}),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
seqno += 1;
|
||||
}
|
||||
Some(pb::agent_server_message::Message::KvServerMessage(kv)) => {
|
||||
handle
|
||||
.command(CursorCommand::Append {
|
||||
seqno,
|
||||
message: Box::new(kv_ack(kv.id)),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
seqno += 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(requested_context);
|
||||
let requests = provider.requests();
|
||||
assert_eq!(requests.len(), 1);
|
||||
let ProjectedContent::Parts(parts) = &requests[0].history[0].content else {
|
||||
panic!("runtime message must use typed parts")
|
||||
};
|
||||
let [ContentPart::Text { text }] = parts.as_slice() else {
|
||||
panic!("this fixture has no images")
|
||||
};
|
||||
assert!(text.contains("<mcp_meta_tool_server name=\"live-mcp\" identifier=\"live-mcp\">"));
|
||||
assert!(text.contains("<mcp_tool name=\"current-tool\">"));
|
||||
|
||||
let stale = references_for(&referenced_context);
|
||||
let current = references_for(¤t_context);
|
||||
for (id, _) in [
|
||||
current.rules,
|
||||
current.skills,
|
||||
current.subagents,
|
||||
current.mcps,
|
||||
] {
|
||||
assert!(store.get_blob(&id).await.unwrap().is_some());
|
||||
}
|
||||
assert!(store.get_blob(&stale.mcps.0).await.unwrap().is_none());
|
||||
}
|
||||
|
||||
struct References {
|
||||
rules: (BlobId, u32),
|
||||
skills: (BlobId, u32),
|
||||
subagents: (BlobId, u32),
|
||||
mcps: (BlobId, u32),
|
||||
}
|
||||
|
||||
async fn references(store: &Store) -> References {
|
||||
let context = fixture_context();
|
||||
let references = references_for(&context);
|
||||
for data in part_data(&context) {
|
||||
store.put_blob(&data, &[]).await.unwrap();
|
||||
}
|
||||
references
|
||||
}
|
||||
|
||||
fn fixture_context() -> pb::RequestContext {
|
||||
pb::RequestContext {
|
||||
rules: vec![pb::CursorRule {
|
||||
full_path: "/workspace/AGENTS.md".into(),
|
||||
content: "workspace rule".into(),
|
||||
..Default::default()
|
||||
}],
|
||||
non_file_rules: vec![pb::CursorRule {
|
||||
full_path: "/skills/test/SKILL.md".into(),
|
||||
content: "complete skill body".into(),
|
||||
..Default::default()
|
||||
}],
|
||||
agent_skills: vec![pb::AgentSkill {
|
||||
full_path: "/skills/test/SKILL.md".into(),
|
||||
description: "test skill".into(),
|
||||
..Default::default()
|
||||
}],
|
||||
custom_subagents: vec![pb::CustomSubagent {
|
||||
name: "reviewer".into(),
|
||||
description: "review code".into(),
|
||||
..Default::default()
|
||||
}],
|
||||
mcp_meta_tool_options: Some(pb::McpMetaToolOptions {
|
||||
enabled: true,
|
||||
mcp_descriptors: vec![pb::McpDescriptor {
|
||||
server_name: "test".into(),
|
||||
server_identifier: "mcp-test".into(),
|
||||
server_use_instructions: Some("complete MCP server instructions".into()),
|
||||
tools: vec![pb::McpToolDescriptor {
|
||||
tool_name: "lookup".into(),
|
||||
definition_path: Some("/tmp/mcp-test/lookup.json".into()),
|
||||
description: Some("look up a value".into()),
|
||||
input_schema_json: Some(
|
||||
r#"{"type":"object","properties":{"query":{"type":"string"}}}"#.into(),
|
||||
),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
}],
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn references_for(context: &pb::RequestContext) -> References {
|
||||
let mut parts = part_data(context).into_iter();
|
||||
References {
|
||||
rules: reference(&parts.next().unwrap()),
|
||||
skills: reference(&parts.next().unwrap()),
|
||||
subagents: reference(&parts.next().unwrap()),
|
||||
mcps: reference(&parts.next().unwrap()),
|
||||
}
|
||||
}
|
||||
|
||||
fn part_data(context: &pb::RequestContext) -> Vec<Vec<u8>> {
|
||||
vec![
|
||||
pb::RequestContextRulesPart {
|
||||
rules: context.rules.clone(),
|
||||
non_file_rules: context.non_file_rules.clone(),
|
||||
cloud_rule: context.cloud_rule.clone(),
|
||||
}
|
||||
.encode_to_vec(),
|
||||
pb::RequestContextSkillsPart {
|
||||
agent_skills: context.agent_skills.clone(),
|
||||
skill_options: context.skill_options.clone(),
|
||||
}
|
||||
.encode_to_vec(),
|
||||
pb::RequestContextSubagentsPart {
|
||||
custom_subagents: context.custom_subagents.clone(),
|
||||
}
|
||||
.encode_to_vec(),
|
||||
pb::RequestContextMcpsPart {
|
||||
tools: context.tools.clone(),
|
||||
mcp_instructions: context.mcp_instructions.clone(),
|
||||
mcp_file_system_options: context.mcp_file_system_options.clone(),
|
||||
mcp_meta_tool_options: context.mcp_meta_tool_options.clone(),
|
||||
}
|
||||
.encode_to_vec(),
|
||||
]
|
||||
}
|
||||
|
||||
fn reference(data: &[u8]) -> (BlobId, u32) {
|
||||
(BlobId::digest(data), data.len() as u32)
|
||||
}
|
||||
|
||||
fn run_request(references: References) -> pb::AgentClientMessage {
|
||||
let (rules, rules_byte_length) = references.rules;
|
||||
let (skills, skills_byte_length) = references.skills;
|
||||
let (subagents, subagents_byte_length) = references.subagents;
|
||||
let (mcps, mcps_byte_length) = references.mcps;
|
||||
pb::AgentClientMessage {
|
||||
message: Some(pb::agent_client_message::Message::RunRequest(
|
||||
pb::AgentRunRequest {
|
||||
conversation_state: Some(pb::ConversationStateStructure {
|
||||
mode: Some(pb::AgentMode::Agent as i32),
|
||||
..Default::default()
|
||||
}),
|
||||
action: Some(pb::ConversationAction {
|
||||
request_context_parts: Some(pb::RequestContextPartReferences {
|
||||
rules_blob_id: rules.as_bytes().to_vec(),
|
||||
rules_byte_length,
|
||||
skills_blob_id: skills.as_bytes().to_vec(),
|
||||
skills_byte_length,
|
||||
subagents_blob_id: subagents.as_bytes().to_vec(),
|
||||
subagents_byte_length,
|
||||
mcps_blob_id: mcps.as_bytes().to_vec(),
|
||||
mcps_byte_length,
|
||||
dynamic_context: Some(pb::RequestContext {
|
||||
env: Some(pb::RequestContextEnv {
|
||||
os_version: "darwin".into(),
|
||||
workspace_paths: vec!["/workspace".into()],
|
||||
shell: "zsh".into(),
|
||||
time_zone: "UTC".into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
}),
|
||||
action: Some(pb::conversation_action::Action::UserMessageAction(
|
||||
pb::UserMessageAction {
|
||||
user_message: Some(pb::UserMessage {
|
||||
text: "explain this".into(),
|
||||
message_id: "wire-user".into(),
|
||||
mode: pb::AgentMode::Ask as i32,
|
||||
selected_context: Some(pb::SelectedContext {
|
||||
invocation_context: Some(pb::InvocationContext {
|
||||
data: Some(pb::invocation_context::Data::IdeState(
|
||||
pb::invocation_context::IdeState {
|
||||
visible_files: vec![
|
||||
pb::invocation_context::ide_state::File {
|
||||
path: "/workspace/src/main.rs".into(),
|
||||
total_lines: 10,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
..Default::default()
|
||||
}),
|
||||
conversation_id: Some("mode-conversation".into()),
|
||||
run_id: Some("wire-run".into()),
|
||||
requested_model: Some(pb::RequestedModel {
|
||||
model_id: "test-model".into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn kv_ack(id: u32) -> pb::AgentClientMessage {
|
||||
pb::AgentClientMessage {
|
||||
message: Some(pb::agent_client_message::Message::KvClientMessage(
|
||||
pb::KvClientMessage {
|
||||
id,
|
||||
message: Some(pb::kv_client_message::Message::SetBlobResult(
|
||||
pb::SetBlobResult { error: None },
|
||||
)),
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn context_stream_close() -> pb::AgentClientMessage {
|
||||
pb::AgentClientMessage {
|
||||
message: Some(pb::agent_client_message::Message::ExecClientControlMessage(
|
||||
pb::ExecClientControlMessage {
|
||||
message: Some(pb::exec_client_control_message::Message::StreamClose(
|
||||
pb::ExecClientStreamClose { id: 0 },
|
||||
)),
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user