yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! Acceptance proof for the inspector composition (§11): build every tab's
//! view-model from one populated fixture workspace and cycle the tab through
//! all five [`InspectorTab`] values, asserting each tab's signature content
//! reaches the paint layer. Plus the governing-config edge arms (present as a
//! branch tip / frozen past it / absent) and the Files placeholder.

use std::collections::HashSet;

use super::*;
use crate::config_edit::branch::{GoverningConfig, governing_config};
use crate::files_view::{self, FilesView};
use crate::git_tree::tests::fixture::Fixture;
use crate::git_tree::{AgentState, GitTree};
use crate::inboxview::list_inbox;
use crate::steps_view::{self, StepTab};
use crate::transcript;

fn screen() -> egui::RawInput {
    egui::RawInput {
        screen_rect: Some(egui::Rect::from_min_size(
            egui::Pos2::ZERO,
            egui::vec2(1024.0, 4096.0),
        )),
        ..Default::default()
    }
}

fn collect_text(shape: &egui::Shape, out: &mut String) {
    match shape {
        egui::Shape::Text(t) => {
            out.push_str(t.galley.text());
            out.push('\n');
        }
        egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| collect_text(s, out)),
        _ => {}
    }
}

/// Render one tab of a [`TabData`] and return every painted galley's text.
fn paint(tab: InspectorTab, data: &TabData) -> String {
    let ctx = egui::Context::default();
    let mut collapsed = HashSet::new();
    let mut file_sel = None;
    let out = ctx.run(screen(), |ctx| {
        egui::CentralPanel::default().show(ctx, |ui| {
            render(ui, tab, data, &mut collapsed, &mut file_sel);
        });
    });
    let mut text = String::new();
    for clipped in out.shapes {
        collect_text(&clipped.shape, &mut text);
    }
    text
}

/// A workspace populated across every inspector surface: a committed agent
/// branch, a `messages/` transcript, a step with a `response.json` + tool i/o,
/// and an inbox deposit.
fn populated() -> (Fixture, String, String) {
    let fx = Fixture::new();
    fx.build_agent("c-1", "hello");
    // Transcript: a delivered message, a model reply, and a tool result.
    let messages = fx.path.join("agents/c-1/messages");
    std::fs::create_dir_all(&messages).unwrap();
    std::fs::write(messages.join("001-user.md"), "please ping").unwrap();
    std::fs::write(
        messages.join("002-opus.json"),
        br#"{"content":[{"type":"text","text":"pong reply"}]}"#,
    )
    .unwrap();
    std::fs::write(
        messages.join("003-tool.json"),
        br#"{"tool_use_id":"toolu_1","content":"tool said hi","is_error":false}"#,
    )
    .unwrap();
    // Step 001 response + tool i/o (drill-in with tool input/output).
    let step = fx.path.join("steps/c-1/001");
    std::fs::create_dir_all(step.join("tools/toolu_1")).unwrap();
    std::fs::write(
        step.join("response.json"),
        b"{\"type\":\"usage\",\"input_tokens\":10,\"output_tokens\":5}\n{\"type\":\"end\"}\n",
    )
    .unwrap();
    std::fs::write(step.join("tools/toolu_1/input.json"), br#"{"name":"Read"}"#).unwrap();
    std::fs::write(
        step.join("tools/toolu_1/output.json"),
        br#"{"exit_code":0}"#,
    )
    .unwrap();
    // Inbox deposit (the `✉n` explanation).
    fx.deposit_message(
        "c-1",
        "user-001.md",
        "---\nfrom: user\ndeposited_at: t0\n---\nfollow-up message",
    );
    let tree = GitTree::from_repo(&fx.path).unwrap();
    let agent = tree.agents.iter().find(|a| a.agent_id == "c-1").unwrap();
    (fx, agent.agent_id.clone(), agent.tip_oid.clone())
}

#[test]
fn collect_text_descends_shape_vec_and_ignores_non_text() {
    // The whole-window fixture renders don't nest galleys in a `Shape::Vec`, so
    // build one directly to cover the walker's recursion arm and its non-text
    // catch-all.
    use egui::{Color32, FontId, Pos2, Stroke};
    let ctx = egui::Context::default();
    let mut nested: Option<egui::Shape> = None;
    let _ = ctx.run(egui::RawInput::default(), |ctx| {
        let galley =
            ctx.fonts(|f| f.layout_no_wrap("nested".into(), FontId::default(), Color32::WHITE));
        nested = Some(egui::Shape::Vec(vec![egui::Shape::Text(
            egui::epaint::TextShape {
                pos: Pos2::ZERO,
                galley,
                underline: Stroke::NONE,
                fallback_color: Color32::WHITE,
                override_text_color: None,
                opacity_factor: 1.0,
                angle: 0.0,
            },
        )]));
    });
    let mut out = String::new();
    collect_text(nested.as_ref().unwrap(), &mut out);
    collect_text(&egui::Shape::Noop, &mut out);
    assert!(out.contains("nested"));
}

#[test]
fn every_tab_paints_its_signature_from_a_populated_fixture() {
    let (fx, agent, tip) = populated();
    let ws = &fx.path;
    let state = GitTree::from_repo(ws)
        .unwrap()
        .agents
        .iter()
        .find(|a| a.agent_id == agent)
        .unwrap()
        .state;
    let in_flight = matches!(state, AgentState::InFlight);
    let transcript = transcript::build(ws, &agent, in_flight);
    let steps = steps_view::build(ws, &agent, state);
    let detail = steps_view::detail(ws, &agent, "001");
    let inbox = list_inbox(ws, &agent);
    let files = files_view::build(ws, &agent);
    // Select goal.md (a work product); its body is the fixture message "hello".
    let preview = files_view::preview(&files_view::agent_worktree(ws, &agent).join("goal.md"));
    let governing = governing_config(ws, &tip).unwrap();
    let governing_short = governing.short_oid.clone();

    let data = TabData {
        transcript,
        raw: false,
        steps,
        step_sel: Some(0),
        step_detail: Some(detail),
        step_tab: StepTab::Tools,
        inbox,
        files,
        file_preview: Some(preview),
        governing: Some(governing),
    };

    // 1 — Transcript: the delivered body, the model reply, the tool result.
    let tx = paint(InspectorTab::Transcript, &data);
    assert!(tx.contains("please ping"), "transcript:\n{tx}");
    assert!(tx.contains("pong reply"));
    assert!(tx.contains("tool said hi"));
    // Raw toggle flips the whole tab to verbatim bytes.
    let raw = paint(
        InspectorTab::Transcript,
        &TabData {
            raw: true,
            ..data.clone()
        },
    );
    assert!(raw.contains("001-user.md"), "raw:\n{raw}");

    // 2 — Steps: the step row (tokens folded) and the tool drill-in i/o.
    let st = paint(InspectorTab::Steps, &data);
    assert!(st.contains("15 tok"), "steps:\n{st}");
    assert!(st.contains("toolu_1"));
    assert!(st.contains("input"));
    assert!(st.contains("output"));

    // 3 — Inbox: the deposit header and body.
    let ib = paint(InspectorTab::Inbox, &data);
    assert!(ib.contains("✉ user · t0"), "inbox:\n{ib}");
    assert!(ib.contains("follow-up message"));

    // 4 — Files: listing + selected-file preview reach paint; root messages/ hidden.
    let fl = paint(InspectorTab::Files, &data);
    assert!(fl.contains("goal.md"), "files tree:\n{fl}");
    assert!(fl.contains("hello"), "goal.md preview reached paint:\n{fl}");
    assert!(!fl.contains("messages"), "root messages/ excluded:\n{fl}");

    // 5 — Config: the governing frozen label and its file listing.
    let cfg = paint(InspectorTab::Config, &data);
    assert!(cfg.contains("policy frozen at"), "config:\n{cfg}");
    assert!(cfg.contains(&governing_short));
    assert!(cfg.contains("version"), "governing files listed:\n{cfg}");
}

/// A [`TabData`] carrying only the given view-models — every ephemeral field at
/// its empty value, the rest built fresh and moved in by the caller.
fn empty_tab_data(
    transcript: Transcript,
    steps: StepsView,
    inbox: Vec<Deposit>,
    files: FilesView,
    governing: Option<GoverningConfig>,
) -> TabData {
    TabData {
        transcript,
        raw: false,
        steps,
        step_sel: None,
        step_detail: None,
        step_tab: StepTab::Meta,
        inbox,
        files,
        file_preview: None,
        governing,
    }
}

#[test]
fn config_tab_without_governing_shows_a_note() {
    let (transcript, steps, inbox) = (Transcript::default(), StepsView::default(), Vec::new());
    let files = FilesView::default();
    let data = empty_tab_data(transcript, steps, inbox, files, None);
    assert!(
        paint(InspectorTab::Config, &data).contains("no governing config"),
        "absent governing note missing"
    );
}

#[test]
fn config_tab_frozen_past_a_branch_tip_omits_the_tip_line() {
    // Fork the agent off config/default, then advance the lineage: the
    // governing commit is now an ancestor, not any branch tip.
    let fx = Fixture::new();
    fx.build_agent("c-1", "hi");
    let forked_tip = {
        let tree = GitTree::from_repo(&fx.path).unwrap();
        tree.agents
            .iter()
            .find(|a| a.agent_id == "c-1")
            .unwrap()
            .tip_oid
            .clone()
    };
    fx.commit_other("providers.yaml", "advanced\n");
    let governing = governing_config(&fx.path, &forked_tip).unwrap();
    assert!(
        governing.branch_name_if_tip_of_one.is_none(),
        "governing should be frozen past the tip"
    );
    let (transcript, steps, inbox) = (Transcript::default(), StepsView::default(), Vec::new());
    let files = FilesView::default();
    let data = empty_tab_data(transcript, steps, inbox, files, Some(governing));
    let cfg = paint(InspectorTab::Config, &data);
    assert!(cfg.contains("policy frozen at"), "config:\n{cfg}");
    assert!(
        !cfg.contains("tip of config/"),
        "no tip line expected:\n{cfg}"
    );
}

#[test]
fn config_tab_at_a_branch_tip_shows_the_tip_line() {
    let governing = GoverningConfig {
        oid: "a".repeat(40),
        short_oid: "aaaaaaaa".into(),
        branch_name_if_tip_of_one: Some("default".into()),
        files: vec!["version".into()],
    };
    let (transcript, steps, inbox) = (Transcript::default(), StepsView::default(), Vec::new());
    let files = FilesView::default();
    let data = empty_tab_data(transcript, steps, inbox, files, Some(governing));
    let cfg = paint(InspectorTab::Config, &data);
    assert!(cfg.contains("tip of config/default"), "config:\n{cfg}");
}