#![allow(dead_code)]
use std::path::Path;
use anyhow::Result;
use crate::domain::providers::ProviderSelection;
use crate::tui::runner::TokenUsageSample;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ArtifactFormat {
#[default]
Markdown,
Html,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GenerationTimingSample {
pub started_at: Option<String>,
pub execution_started_at: Option<String>,
pub finished_at: Option<String>,
pub duration_ms: Option<u128>,
pub reasoning_duration_ms: Option<u128>,
pub execution_duration_ms: Option<u128>,
pub provider_reported: bool,
}
#[derive(Clone, Debug)]
pub struct Telemetry {
pub provider: String,
pub model: String,
pub effort: String,
pub offline: bool,
pub usage: Option<TokenUsageSample>,
pub timing: Option<GenerationTimingSample>,
}
pub struct GenerationContext<'a> {
pub stage: &'a str,
pub name: &'a str,
pub input: &'a str,
pub root: &'a Path,
pub provider: &'a ProviderSelection,
pub format: ArtifactFormat,
pub real_telemetry: Option<Telemetry>,
pub no_telemetry: bool,
}
impl GenerationContext<'_> {
pub fn should_emit_telemetry(&self) -> bool {
self.real_telemetry.is_some() && !self.no_telemetry
}
}
pub trait ArtifactGenerator {
fn generate_markdown(&self, ctx: &GenerationContext<'_>) -> Result<String>;
#[cfg(feature = "html-gen")]
fn generate_html(&self, ctx: &GenerationContext<'_>) -> Result<String>;
}
pub struct DeterministicGenerator;
pub struct ProviderGenerator;
impl ArtifactGenerator for DeterministicGenerator {
fn generate_markdown(&self, ctx: &GenerationContext<'_>) -> Result<String> {
crate::render_stage_artifact_with_ctx(ctx)
}
#[cfg(feature = "html-gen")]
fn generate_html(&self, ctx: &GenerationContext<'_>) -> Result<String> {
let markdown = self.generate_markdown(ctx)?;
crate::domain::html::StageHtmlGenerator::new(&markdown, ctx.stage, ctx.name).render()
}
}
impl ArtifactGenerator for ProviderGenerator {
fn generate_markdown(&self, ctx: &GenerationContext<'_>) -> Result<String> {
crate::render_stage_artifact_with_ctx(ctx)
}
#[cfg(feature = "html-gen")]
fn generate_html(&self, ctx: &GenerationContext<'_>) -> Result<String> {
let markdown = self.generate_markdown(ctx)?;
crate::domain::html::StageHtmlGenerator::new(&markdown, ctx.stage, ctx.name).render()
}
}
pub enum StageGenerator {
Deterministic(DeterministicGenerator),
Provider(ProviderGenerator),
}
impl StageGenerator {
pub fn select(ctx: &GenerationContext<'_>) -> Self {
match ctx.real_telemetry {
Some(_) => StageGenerator::Provider(ProviderGenerator),
None => StageGenerator::Deterministic(DeterministicGenerator),
}
}
}
impl ArtifactGenerator for StageGenerator {
fn generate_markdown(&self, ctx: &GenerationContext<'_>) -> Result<String> {
match self {
StageGenerator::Deterministic(g) => g.generate_markdown(ctx),
StageGenerator::Provider(g) => g.generate_markdown(ctx),
}
}
#[cfg(feature = "html-gen")]
fn generate_html(&self, ctx: &GenerationContext<'_>) -> Result<String> {
match self {
StageGenerator::Deterministic(g) => g.generate_html(ctx),
StageGenerator::Provider(g) => g.generate_html(ctx),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::{Path, PathBuf};
fn dummy_provider() -> ProviderSelection {
use std::collections::BTreeMap;
ProviderSelection {
id: "anthropic".to_string(),
kind: "claude".to_string(),
model: "claude-sonnet-4-5".to_string(),
effort: "normal".to_string(),
offline: false,
enabled: true,
auth_env: None,
auth_present: None,
auth_methods: vec![],
models: vec![],
efforts: vec![],
stage_models: BTreeMap::new(),
stage_efforts: BTreeMap::new(),
capabilities: vec![],
token_budget: None,
usage_limits: vec![],
explicit: false,
}
}
fn ctx_with_telemetry<'a>(
provider: &'a ProviderSelection,
root: &'a Path,
telemetry: Option<Telemetry>,
no_telemetry: bool,
) -> GenerationContext<'a> {
GenerationContext {
stage: "idea",
name: "Test Orchestration",
input: "input de teste",
root,
provider,
format: ArtifactFormat::Markdown,
real_telemetry: telemetry,
no_telemetry,
}
}
fn dummy_telemetry() -> Telemetry {
Telemetry {
provider: "anthropic".to_string(),
model: "claude-sonnet-4-5".to_string(),
effort: "normal".to_string(),
offline: false,
usage: None,
timing: None,
}
}
#[test]
fn should_emit_telemetry_none_no_flag() {
let provider = dummy_provider();
let root = PathBuf::from("/tmp");
let ctx = ctx_with_telemetry(&provider, &root, None, false);
assert!(
!ctx.should_emit_telemetry(),
"sem telemetria real e sem flag: não deve emitir"
);
}
#[test]
fn should_emit_telemetry_none_with_flag() {
let provider = dummy_provider();
let root = PathBuf::from("/tmp");
let ctx = ctx_with_telemetry(&provider, &root, None, true);
assert!(
!ctx.should_emit_telemetry(),
"sem telemetria real + no_telemetry=true: não deve emitir"
);
}
#[test]
fn should_emit_telemetry_some_no_flag() {
let provider = dummy_provider();
let root = PathBuf::from("/tmp");
let ctx = ctx_with_telemetry(&provider, &root, Some(dummy_telemetry()), false);
assert!(
ctx.should_emit_telemetry(),
"telemetria real disponível e sem flag: deve emitir"
);
}
#[test]
fn should_emit_telemetry_some_with_flag() {
let provider = dummy_provider();
let root = PathBuf::from("/tmp");
let ctx = ctx_with_telemetry(&provider, &root, Some(dummy_telemetry()), true);
assert!(
!ctx.should_emit_telemetry(),
"telemetria real disponível + no_telemetry=true: não deve emitir"
);
}
#[test]
fn stage_generator_select_deterministic_when_no_telemetry() {
let provider = dummy_provider();
let root = PathBuf::from("/tmp");
let ctx = ctx_with_telemetry(&provider, &root, None, false);
let gen = StageGenerator::select(&ctx);
assert!(
matches!(gen, StageGenerator::Deterministic(_)),
"sem telemetria real: deve selecionar DeterministicGenerator"
);
}
#[test]
fn stage_generator_select_provider_when_telemetry_present() {
let provider = dummy_provider();
let root = PathBuf::from("/tmp");
let ctx = ctx_with_telemetry(&provider, &root, Some(dummy_telemetry()), false);
let gen = StageGenerator::select(&ctx);
assert!(
matches!(gen, StageGenerator::Provider(_)),
"com telemetria real: deve selecionar ProviderGenerator"
);
}
#[test]
fn artifact_format_default_is_markdown() {
assert_eq!(ArtifactFormat::default(), ArtifactFormat::Markdown);
}
#[test]
fn generation_context_fields_accessible() {
let provider = dummy_provider();
let root = PathBuf::from("/tmp");
let ctx = GenerationContext {
stage: "prd",
name: "Minha Feature",
input: "descrição da feature",
root: root.as_path(),
provider: &provider,
format: ArtifactFormat::Html,
real_telemetry: None,
no_telemetry: false,
};
assert_eq!(ctx.stage, "prd");
assert_eq!(ctx.format, ArtifactFormat::Html);
assert!(!ctx.should_emit_telemetry());
}
#[test]
fn parity_cli_tui_deterministic_generate_markdown_byte_identical() {
let provider = dummy_provider();
let root = PathBuf::from("/tmp");
let ctx = GenerationContext {
stage: "idea",
name: "parity-test-orchestration",
input: "entrada de paridade determinística",
root: root.as_path(),
provider: &provider,
format: ArtifactFormat::Markdown,
real_telemetry: None,
no_telemetry: false,
};
let cli_output = StageGenerator::select(&ctx)
.generate_markdown(&ctx)
.expect("CLI path deve gerar Markdown sem erro");
let tui_equivalent_output = StageGenerator::select(&ctx)
.generate_markdown(&ctx)
.expect("TUI equivalent path deve gerar Markdown sem erro");
assert_eq!(
cli_output, tui_equivalent_output,
"T-08 paridade CLI=TUI: saída deve ser byte-idêntica para o mesmo GenerationContext"
);
assert!(
cli_output.contains("# "),
"artefato deve ter título de nível 1"
);
assert!(
cli_output.contains("parity-test-orchestration"),
"artefato deve referenciar o nome da orquestração"
);
assert!(
cli_output.contains("## Rastreabilidade"),
"artefato deve conter seção de rastreabilidade"
);
}
}