use std::sync::Arc;
use lingshu_core::agent::{AgentBuilder, ApprovalChoice, StreamEvent};
use lingshu_tools::registry::ToolRegistry;
use edgequake_llm::providers::vscode::{auth::GitHubAuth, token::TokenManager};
use tempfile::TempDir;
const COPILOT_TEST_MODEL: &str = "gpt-5-mini";
const COPILOT_TEST_SPEC: &str = "vscode-copilot/gpt-5-mini";
const COPILOT_AUTO_MODEL: &str = "auto";
const COPILOT_AUTO_SPEC: &str = "vscode-copilot/auto";
fn copilot_available() -> bool {
if std::env::var("VSCODE_IPC_HOOK_CLI").is_ok() || std::env::var("VSCODE_COPILOT_TOKEN").is_ok()
{
return true;
}
if let Some(home) = dirs::home_dir() {
let candidates = [
home.join(".config/github-copilot/hosts.json"),
home.join("Library/Application Support/github-copilot/hosts.json"),
home.join(".config/edgequake/copilot/github_token.json"),
home.join("Library/Application Support/edgequake/copilot/github_token.json"),
];
return candidates.iter().any(|path| path.exists());
}
false
}
fn is_verified_global_rate_limit(message: &str) -> bool {
let msg = message.to_ascii_lowercase();
msg.contains("user_weekly_rate_limited")
|| msg.contains("user_global_rate_limited")
|| msg.contains("global-chat:global-cogs-7-day-key")
}
fn needs_copilot_device_login(message: &str) -> bool {
let msg = message.to_ascii_lowercase();
msg.contains("no github copilot oauth session found")
|| msg.contains("run `lingshu auth login copilot`")
|| msg.contains("bad credentials")
|| msg.contains("rejected by github")
|| msg.contains("401 unauthorized")
}
#[test]
fn detects_when_e2e_should_trigger_device_login() {
assert!(needs_copilot_device_login(
"No GitHub Copilot OAuth session found. Run `lingshu auth login copilot`."
));
assert!(needs_copilot_device_login(
"All available GitHub Copilot credentials were rejected by GitHub"
));
assert!(!needs_copilot_device_login(
"user_weekly_rate_limited: retry after 1d 21h"
));
}
async fn ensure_copilot_authenticated_for_e2e() {
let manager = TokenManager::new().expect("token manager should initialize");
match manager.get_valid_copilot_token().await {
Ok(_) => return,
Err(err) => {
let msg = err.to_string();
if !needs_copilot_device_login(&msg) {
eprintln!(
"Continuing Copilot E2E without interactive re-login; the remaining issue is not an auth prompt: {msg}"
);
return;
}
eprintln!(
"Copilot E2E needs an official GitHub device login before the live request can continue: {msg}"
);
}
}
let auth = GitHubAuth::new().expect("GitHub auth client should initialize");
let access_token = auth
.device_code_flow(|code| {
let url = code
.verification_uri_complete
.as_deref()
.unwrap_or(&code.verification_uri);
eprintln!("\nOpen this link to authenticate GitHub Copilot for the E2E test:\n{url}");
eprintln!("Code: {}\n", code.user_code);
eprintln!("Waiting for GitHub approval...\n");
})
.await
.expect("device login should complete for the opted-in E2E run");
manager
.save_github_token(access_token)
.await
.expect("fresh GitHub token should be cached after device login");
let _ = manager
.get_valid_copilot_token()
.await
.expect("Copilot token refresh should succeed after device login");
}
#[tokio::test]
#[ignore = "requires VS Code Copilot (VSCODE_IPC_HOOK_CLI or VSCODE_COPILOT_TOKEN)"]
async fn e2e_agent_chat_with_copilot_gpt4_mini() {
if !copilot_available() {
eprintln!("Skipping: VS Code Copilot not available");
return;
}
ensure_copilot_authenticated_for_e2e().await;
let provider = lingshu_tools::create_provider_for_model("vscode-copilot", COPILOT_TEST_MODEL)
.expect("should create VsCodeCopilot provider");
let agent = AgentBuilder::new(COPILOT_TEST_SPEC)
.provider(provider)
.max_iterations(3)
.build()
.expect("agent build should succeed");
match agent.chat("Reply with exactly: PONG").await {
Ok(response) => {
assert!(!response.is_empty(), "Response should not be empty");
assert!(
response.to_uppercase().contains("PONG"),
"Expected PONG in response, got: {response}"
);
}
Err(err) => {
let msg = err.to_string();
if is_verified_global_rate_limit(&msg) {
eprintln!(
"Skipping live Copilot round-trip because GitHub auth succeeded but the account is currently under an upstream Copilot weekly/global rate limit: {msg}"
);
return;
}
panic!("chat should succeed: {msg}");
}
}
}
#[tokio::test]
#[ignore = "requires VS Code Copilot (VSCODE_IPC_HOOK_CLI or VSCODE_COPILOT_TOKEN)"]
async fn e2e_agent_chat_with_copilot_gpt5_mini() {
if !copilot_available() {
eprintln!("Skipping: VS Code Copilot not available");
return;
}
ensure_copilot_authenticated_for_e2e().await;
let provider = lingshu_tools::create_provider_for_model("vscode-copilot", COPILOT_TEST_MODEL)
.expect("should create VsCodeCopilot provider");
let agent = AgentBuilder::new(COPILOT_TEST_SPEC)
.provider(provider)
.max_iterations(3)
.build()
.expect("agent build should succeed");
match agent
.chat("Reply with exactly: EDGECRAB_GPT5_MINI_OK")
.await
{
Ok(response) => {
println!("GPT-5 mini response: {response}");
assert!(!response.is_empty(), "Response should not be empty");
assert!(
response.to_uppercase().contains("EDGECRAB_GPT5_MINI_OK"),
"Expected EDGECRAB_GPT5_MINI_OK in response, got: {response}"
);
}
Err(err) => {
let msg = err.to_string();
if is_verified_global_rate_limit(&msg) {
eprintln!(
"Skipping gpt-5-mini due to upstream Copilot weekly/global rate limit: {msg}"
);
return;
}
panic!("gpt-5-mini chat should succeed: {msg}");
}
}
}
#[tokio::test]
#[ignore = "requires VS Code Copilot (VSCODE_IPC_HOOK_CLI or VSCODE_COPILOT_TOKEN)"]
async fn e2e_agent_chat_with_copilot_auto() {
if !copilot_available() {
eprintln!("Skipping: VS Code Copilot not available");
return;
}
ensure_copilot_authenticated_for_e2e().await;
let provider = lingshu_tools::create_provider_for_model("vscode-copilot", COPILOT_AUTO_MODEL)
.expect("should create VsCodeCopilot provider for Auto");
let agent = AgentBuilder::new(COPILOT_AUTO_SPEC)
.provider(provider)
.max_iterations(3)
.build()
.expect("agent build should succeed");
match agent.chat("Reply with exactly: EDGECRAB_AUTO_OK").await {
Ok(response) => {
println!("Auto response: {response}");
assert!(!response.is_empty(), "Response should not be empty");
assert!(
response.to_uppercase().contains("EDGECRAB_AUTO_OK"),
"Expected EDGECRAB_AUTO_OK in response, got: {response}"
);
}
Err(err) => {
let msg = err.to_string();
if is_verified_global_rate_limit(&msg) {
eprintln!(
"Skipping auto-mode due to upstream Copilot weekly/global rate limit: {msg}"
);
return;
}
panic!("auto-mode chat should succeed: {msg}");
}
}
}
#[tokio::test]
#[ignore = "requires VS Code Copilot (VSCODE_IPC_HOOK_CLI or VSCODE_COPILOT_TOKEN)"]
async fn e2e_agent_streaming_with_copilot() {
if !copilot_available() {
eprintln!("Skipping: VS Code Copilot not available");
return;
}
ensure_copilot_authenticated_for_e2e().await;
let provider = lingshu_tools::create_provider_for_model("vscode-copilot", COPILOT_TEST_MODEL)
.expect("should create VsCodeCopilot provider");
let agent = Arc::new(
AgentBuilder::new(COPILOT_TEST_SPEC)
.provider(provider)
.max_iterations(2)
.build()
.expect("agent build should succeed"),
);
let (chunk_tx, mut chunk_rx) = tokio::sync::mpsc::unbounded_channel::<StreamEvent>();
let agent_clone = Arc::clone(&agent);
let handle = tokio::spawn(async move {
match agent_clone
.chat_streaming("Count from 1 to 3 with spaces.", chunk_tx)
.await
{
Ok(()) => {}
Err(err) => {
let msg = err.to_string();
if is_verified_global_rate_limit(&msg) {
eprintln!(
"Skipping streaming due to upstream Copilot weekly/global rate limit: {msg}"
);
return;
}
panic!("streaming should succeed: {msg}");
}
}
});
let mut accumulated = String::new();
let mut got_done = false;
while let Some(event) = chunk_rx.recv().await {
match event {
StreamEvent::Token(text) => accumulated.push_str(&text),
StreamEvent::ToolGenerating { .. } => {} StreamEvent::ToolExec { .. } => {} StreamEvent::ToolProgress { .. } => {} StreamEvent::ToolDone { .. } => {} StreamEvent::SubAgentStart { .. } => {} StreamEvent::SubAgentReasoning { .. } => {} StreamEvent::SubAgentToolExec { .. } => {} StreamEvent::SubAgentFinish { .. } => {} StreamEvent::RunFinished { .. } => {} StreamEvent::Done => {
got_done = true;
break;
}
StreamEvent::Clarify { .. } => {} StreamEvent::Error(e) => {
if is_verified_global_rate_limit(&e) {
eprintln!(
"Skipping streaming due to upstream Copilot weekly/global rate limit: {e}"
);
return;
}
panic!("Unexpected streaming error: {e}")
}
StreamEvent::Reasoning(_) => {} StreamEvent::HookEvent { .. } => {} StreamEvent::ContextPressure { .. } => {} StreamEvent::Approval { response_tx, .. } => {
let _ = response_tx.send(ApprovalChoice::Deny);
}
StreamEvent::SecretRequest { response_tx, .. } => {
let _ = response_tx.send(String::new());
}
StreamEvent::SteerPending { .. } => {} StreamEvent::SteerApplied { .. } => {} StreamEvent::ActivityNotice { .. } => {} StreamEvent::LlmWaitProgress { .. } => {} StreamEvent::BackgroundProcessTail { .. } => {} StreamEvent::BackgroundProcessFinished { .. } => {} StreamEvent::ModelTransferComplete { .. } => {} StreamEvent::Footer(text) => accumulated.push_str(&text),
}
}
handle.await.expect("spawn should complete");
assert!(got_done, "Should receive Done event");
assert!(!accumulated.is_empty(), "Should receive tokens");
let has_numbers = accumulated.contains('1') && accumulated.contains('3');
assert!(has_numbers, "Expected 1..3 in response, got: {accumulated}");
}
#[tokio::test]
#[ignore = "requires VS Code Copilot (VSCODE_IPC_HOOK_CLI or VSCODE_COPILOT_TOKEN)"]
async fn e2e_model_swap_with_copilot() {
if !copilot_available() {
eprintln!("Skipping: VS Code Copilot not available");
return;
}
ensure_copilot_authenticated_for_e2e().await;
let provider = lingshu_tools::create_provider_for_model("vscode-copilot", COPILOT_TEST_MODEL)
.expect("copilot provider");
let agent = AgentBuilder::new(COPILOT_TEST_SPEC)
.provider(Arc::clone(&provider))
.max_iterations(2)
.build()
.expect("build");
let r1 = match agent.chat("Say ALPHA").await {
Ok(res) => res,
Err(err) => {
let msg = err.to_string();
if is_verified_global_rate_limit(&msg) {
eprintln!(
"Skipping model swap due to upstream Copilot weekly/global rate limit: {msg}"
);
return;
}
panic!("first chat: {msg}");
}
};
assert!(r1.to_uppercase().contains("ALPHA"), "got: {r1}");
let provider2 = lingshu_tools::create_provider_for_model("vscode-copilot", COPILOT_TEST_MODEL)
.expect("second provider");
agent.swap_model(COPILOT_TEST_SPEC.into(), provider2).await;
let r2 = match agent.chat("Say BETA").await {
Ok(res) => res,
Err(err) => {
let msg = err.to_string();
if is_verified_global_rate_limit(&msg) {
eprintln!(
"Skipping model swap due to upstream Copilot weekly/global rate limit: {msg}"
);
return;
}
panic!("second chat: {msg}");
}
};
assert!(r2.to_uppercase().contains("BETA"), "got: {r2}");
}
#[tokio::test]
#[ignore = "requires VS Code Copilot (VSCODE_IPC_HOOK_CLI or VSCODE_COPILOT_TOKEN)"]
async fn e2e_file_mutation_verifier_footer_with_copilot() {
if !copilot_available() {
eprintln!("Skipping: VS Code Copilot not available");
return;
}
ensure_copilot_authenticated_for_e2e().await;
let workspace = TempDir::new().expect("temp workspace");
let provider = lingshu_tools::create_provider_for_model("vscode-copilot", COPILOT_TEST_MODEL)
.expect("should create VsCodeCopilot provider");
let agent = AgentBuilder::new(COPILOT_TEST_SPEC)
.provider(provider)
.tools(Arc::new(ToolRegistry::new()))
.max_iterations(12)
.build()
.expect("agent build should succeed");
let prompt = "Use the write_file tool exactly once to create the file \
`mutation_e2e_probe.txt` with content `PROBE_LIVE` in the current working \
directory. Do not use any other tools. After write_file succeeds, reply with \
exactly: MUTATION_E2E_DONE";
let result = match agent
.run_conversation_in_cwd(prompt, None, None, workspace.path())
.await
{
Ok(result) => result,
Err(err) => {
let msg = err.to_string();
if is_verified_global_rate_limit(&msg) {
eprintln!("Skipping live mutation verifier E2E (rate limit): {msg}");
return;
}
panic!("conversation failed: {msg}");
}
};
let probe = workspace.path().join("mutation_e2e_probe.txt");
assert!(
probe.is_file(),
"write_file should create mutation_e2e_probe.txt in workspace"
);
assert_eq!(
std::fs::read_to_string(&probe).expect("read probe"),
"PROBE_LIVE"
);
let history_has_footer = result.messages.iter().any(|m| {
m.text_content().contains("files-mutated")
|| m.text_content().contains("[file-mutation-verifier]")
});
assert!(
result.final_response.contains("files-mutated") || history_has_footer,
"expected mutation footer in final response or history; final_response={}",
result.final_response
);
assert!(
result.final_response.contains("MUTATION_E2E_DONE")
|| result.final_response.to_uppercase().contains("DONE"),
"expected completion marker, got: {}",
result.final_response
);
println!("Live mutation verifier E2E certified.");
let tail = result.final_response.len().saturating_sub(500);
println!("Response tail:\n{}", &result.final_response[tail..]);
}
#[tokio::test]
#[ignore = "requires VS Code Copilot (VSCODE_IPC_HOOK_CLI or VSCODE_COPILOT_TOKEN)"]
async fn e2e_file_mutation_verifier_stream_footer_with_copilot() {
if !copilot_available() {
eprintln!("Skipping: VS Code Copilot not available");
return;
}
ensure_copilot_authenticated_for_e2e().await;
let workspace = TempDir::new().expect("temp workspace");
let previous_cwd = std::env::current_dir().ok();
std::env::set_current_dir(workspace.path()).expect("chdir to temp workspace");
let provider = lingshu_tools::create_provider_for_model("vscode-copilot", COPILOT_TEST_MODEL)
.expect("should create VsCodeCopilot provider");
let agent = Arc::new(
AgentBuilder::new(COPILOT_TEST_SPEC)
.provider(provider)
.tools(Arc::new(ToolRegistry::new()))
.max_iterations(12)
.build()
.expect("agent build should succeed"),
);
let prompt = "Use write_file once to create `mutation_stream_probe.txt` with content `STREAM_PROBE`. \
Then reply exactly: STREAM_OK";
let (chunk_tx, mut chunk_rx) = tokio::sync::mpsc::unbounded_channel::<StreamEvent>();
let agent_clone = Arc::clone(&agent);
let prompt = prompt.to_string();
let handle = tokio::spawn(async move {
match agent_clone.chat_streaming(&prompt, chunk_tx).await {
Ok(()) => {}
Err(err) => {
let msg = err.to_string();
if is_verified_global_rate_limit(&msg) {
eprintln!("Skipping stream mutation E2E (rate limit): {msg}");
return;
}
panic!("chat_streaming failed: {msg}");
}
}
});
let mut footer = String::new();
let mut got_done = false;
while let Some(event) = chunk_rx.recv().await {
match event {
StreamEvent::Footer(text) => footer = text,
StreamEvent::Done => {
got_done = true;
break;
}
StreamEvent::Error(e) => {
if is_verified_global_rate_limit(&e) {
eprintln!("Skipping stream mutation E2E (rate limit): {e}");
return;
}
panic!("stream error: {e}");
}
StreamEvent::Approval { response_tx, .. } => {
let _ = response_tx.send(ApprovalChoice::Once);
}
StreamEvent::Clarify { response_tx, .. } => {
let _ = response_tx.send("yes".into());
}
StreamEvent::SecretRequest { response_tx, .. } => {
let _ = response_tx.send(String::new());
}
_ => {}
}
}
handle.await.expect("join streaming task");
assert!(got_done, "should receive Done");
assert!(
footer.contains("files-mutated"),
"Footer event should carry success log, got: {footer}"
);
assert!(
workspace.path().join("mutation_stream_probe.txt").is_file(),
"stream path should still write the probe file"
);
println!("Stream Footer event:\n{footer}");
if let Some(prev) = previous_cwd {
let _ = std::env::set_current_dir(prev);
}
}
#[tokio::test]
#[ignore = "requires VS Code Copilot (VSCODE_IPC_HOOK_CLI or VSCODE_COPILOT_TOKEN)"]
async fn e2e_lsp_write_diagnostics_with_copilot_gpt5_mini() {
if !copilot_available() {
eprintln!("Skipping: VS Code Copilot not available");
return;
}
ensure_copilot_authenticated_for_e2e().await;
let workspace = TempDir::new().expect("temp workspace");
std::fs::create_dir_all(workspace.path().join("src")).expect("src dir");
std::fs::write(
workspace.path().join("Cargo.toml"),
"[package]\nname = \"lsp_e2e\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.expect("cargo.toml");
let provider = lingshu_tools::create_provider_for_model("vscode-copilot", COPILOT_TEST_MODEL)
.expect("should create VsCodeCopilot provider");
let agent = AgentBuilder::new(COPILOT_TEST_SPEC)
.provider(provider)
.tools(Arc::new(ToolRegistry::new()))
.max_iterations(12)
.build()
.expect("agent build should succeed");
let prompt = "Use the write_file tool exactly once to create `src/lsp_e2e_broken.rs` with this \
exact content (one line): fn main() { let _: i32 = \"not an int\"; }\n\
Do not use any other tools. After write_file succeeds, reply with exactly: \
LSP_E2E_DONE";
let result = match agent
.run_conversation_in_cwd(prompt, None, None, workspace.path())
.await
{
Ok(result) => result,
Err(err) => {
let msg = err.to_string();
if is_verified_global_rate_limit(&msg) {
eprintln!("Skipping LSP write diagnostics E2E (rate limit): {msg}");
return;
}
panic!("conversation failed: {msg}");
}
};
let broken = workspace.path().join("src/lsp_e2e_broken.rs");
assert!(
broken.is_file(),
"write_file should create src/lsp_e2e_broken.rs"
);
let tool_has_lsp = result.messages.iter().any(|m| {
m.role == lingshu_types::Role::Tool
&& (m.text_content().contains("\"diagnostics\"")
|| m.text_content().contains("lsp_diagnostics")
|| m.text_content().contains("<diagnostics"))
});
assert!(
tool_has_lsp,
"write_file tool result should include LSP diagnostics when lsp.enabled; messages={:?}",
result
.messages
.iter()
.filter(|m| m.role == lingshu_types::Role::Tool)
.map(|m| m.text_content())
.collect::<Vec<_>>()
);
assert!(
result.final_response.contains("LSP_E2E_DONE")
|| result.final_response.to_uppercase().contains("DONE"),
"expected completion marker, got: {}",
result.final_response
);
println!("Live LSP write diagnostics E2E certified with Copilot gpt-5-mini.");
}