Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crates/btv-cli/src/btv_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,9 @@ fn spawn_status_watcher(state: BtvAgentState, task_id: String, ctx: btv_domain::
let now = crate::session::now_rfc3339();
{
let store = state.store.lock().unwrap_or_else(|e| e.into_inner());
let _ = store.set_status(&task_id, status, &now);
if let Err(e) = store.set_status(&task_id, status, &now) {
eprintln!("btv: falha ao persistir status final do run ({task_id}): {e}");
}
}
// Entregas (U4): só em conclusão limpa — run com erro/encerrado não
// "entrega". A escrituração vive em `registrar_entregas`, extraída para
Expand Down
4 changes: 3 additions & 1 deletion crates/btv-cli/src/squad_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,9 @@ where
}
}
}
let _ = ledger_session.finish(failure.is_none(), 1);
if let Err(e) = ledger_session.finish(failure.is_none(), 1) {
eprintln!("btv: falha ao finalizar a sessão do squad no ledger: {e}");
}
core_task.abort();
match failure {
None => Ok(()),
Expand Down
4 changes: 3 additions & 1 deletion crates/btv-cli/src/web_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,9 @@ fn finish_task_ok(
ledger_session: &mut crate::session::Session,
steps: usize,
) {
let _ = ledger_session.finish(true, steps);
if let Err(e) = ledger_session.finish(true, steps) {
eprintln!("btv: falha ao finalizar a sessão no ledger ({session_id}): {e}");
}
let verified = ledger_session.verify().unwrap_or(0);
// Libera ANTES de anunciar: `Done` é o sinal de "pode mandar a próxima"
// — publicá-lo com a sessão ainda `busy` abria uma janela de 409 para o
Expand Down
75 changes: 75 additions & 0 deletions crates/btv-llm/src/gateway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,79 @@ mod tests {
.unwrap_err();
assert!(matches!(err, GatewayError::NoProvider));
}

fn req_simples() -> GenerateRequest {
GenerateRequest {
model: "x".into(),
system: String::new(),
messages: vec![],
tools: vec![],
max_tokens: 16,
temperature: None,
}
}

/// T3 — falha REAL de transporte (conexão recusada), não mock: um provider
/// apontado a uma porta fechada vira `AllFailed` (a cadeia de fallback tenta
/// e reporta), nunca pendura nem entra em pânico.
#[tokio::test]
async fn conexao_recusada_vira_all_failed() {
// Porta livre obtida e liberada na hora → conexão recusada determinística.
let porta = {
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
l.local_addr().unwrap().port()
};
let gw = Gateway {
client: reqwest::Client::new(),
providers: vec![ProviderConfig {
id: ProviderId::Anthropic,
api_key: "k".into(),
base_url: format!("http://127.0.0.1:{porta}"),
}],
};
let err = gw.generate(req_simples(), &mut |_| {}).await.unwrap_err();
let GatewayError::AllFailed(msg) = err else {
panic!("esperava AllFailed em conexão recusada");
};
assert!(
msg.contains("anthropic"),
"mensagem deve nomear o provider: {msg}"
);
}

/// T3 — provider que ACEITA a conexão mas nunca responde: o timeout do
/// cliente HTTP corta em vez de esperar para sempre (o bug de produção que
/// os timeouts do gateway resolveram — squad "ativa" zumbi). Vira `AllFailed`
/// rápido, sem pendurar.
#[tokio::test]
async fn servidor_que_pendura_estoura_timeout_sem_pendurar() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let porta = listener.local_addr().unwrap().port();
tokio::spawn(async move {
// Aceita e segura o socket sem escrever resposta HTTP.
if let Ok((sock, _)) = listener.accept().await {
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
drop(sock);
}
});
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_millis(400))
.build()
.unwrap();
let gw = Gateway {
client,
providers: vec![ProviderConfig {
id: ProviderId::Anthropic,
api_key: "k".into(),
base_url: format!("http://127.0.0.1:{porta}"),
}],
};
let inicio = std::time::Instant::now();
let err = gw.generate(req_simples(), &mut |_| {}).await.unwrap_err();
assert!(
inicio.elapsed() < std::time::Duration::from_secs(5),
"deveria cortar no timeout curto, não esperar o servidor"
);
assert!(matches!(err, GatewayError::AllFailed(_)));
}
}
4 changes: 2 additions & 2 deletions docs/documentacao/mapeamentos/04-cobertura-de-testes.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Playwright) + análise de código.
| btv-domain | U | ✅ (agregado, máquina de status, round-trip) | ✅ transições inválidas, tenant vazio | baixo |
| btv-schemas | U, P, G, B | ✅ | ✅ `NumeroProibido`, paridade de hash, fixtures | baixo |
| btv-core | U | ✅ loop completo <100ms, negação, desconhecida, truncado | ✅ `MaxSteps`, deny | baixo |
| btv-llm | U, B | ✅ agregadores, tier, rate-limit | ⚠️ falha de rede real do provedor não é unit-testável (mock) | médio |
| btv-llm | U, B | ✅ agregadores, tier, rate-limit | falha REAL de transporte (conexão recusada) e timeout (servidor que pendura, corta rápido) → `AllFailed` | baixo |
| btv-tools | U, I (`loop_com_ferramentas_reais`, `mcp_integration`, `lsp_integration`) | ✅ tools reais, MCP/LSP contra servidor real | ⚠️ sandbox Docker: testes `#[ignore]` (só no job `sandbox`) | médio |
| btv-store | U, C (`contract_sqlite`, `contract_pg`), I (migrações, replay) | ✅ | ✅ `BrokenChain`/`ForeignEntry`/`Conflict`, RLS adversarial, retry concorrente | baixo |
| btv-verify | U, G (`schema_golden`) | ✅ pipeline, vetter, kill de grupo | ✅ timeout, block fail-closed | baixo |
Expand Down Expand Up @@ -59,7 +59,7 @@ Playwright) + análise de código.
|---|---|---|---|---|
| `btv-cli::web_agent` ramos de erro (permissão timeout, 409 ator único, sessão morta) | e2e parcial | **alto** | unit do `SessionHub` p/ timeout→Deny e single-actor | real |
| `btv-cli::squad_agent` (cockpit inject, HITL expirado, emergency-stop) | e2e | **médio-alto** | teste do `inject_cockpit_context` e do gate obsoleto | real |
| `Gateway` falha real de rede/timeout do provedor | mock | médio | teste com servidor HTTP fake lento (timeout) | real |
| `Gateway` falha real de rede/timeout do provedor | ✅ unit (conexão recusada + servidor que pendura c/ timeout curto) → `AllFailed` | baixo | coberto (`gateway.rs`) | real |
| `Sandbox` contenção (escape, rede, mem) | `#[ignore]` (job `sandbox` com Docker) | médio | já existe — exige daemon | real |
| `PgStore` sob contenção extrema (>64 retries) | contract_pg | baixo-médio | teste que force >64 adversários | teórico (defesa) |
| `BrokenChain` real em produção | unit | baixo | já coberto | teórico (não deve ocorrer) |
Expand Down
Loading