yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! Full-window acceptance smoke (§11): render the whole shell — the top bar
//! (attention strip + tab bar), the conversation list, the selected
//! conversation center with its inline Login banner, the composer, the
//! activity accessory, and the per-agent inspector — over a populated fixture
//! workspace, cycling every inspector tab and config mode, and assert each
//! data surface reaches the paint layer. This drives the coverage-excluded
//! shell glue end to end, so it is the reachability proof that complements the
//! per-widget render tests.

use super::{ShellState, render};
use crate::AppModel;
use crate::app::Roots;
use crate::cli_outbound::Cli;
use crate::git_tree::tests::fixture::Fixture;
use crate::keymap::InspectorTab;
use crate::projects::runner::BlCli;
use crate::ui_state::SystemClock;
use crate::xdg::Env;
use std::path::PathBuf;
use std::sync::Arc;
use tempfile::{TempDir, tempdir};

/// A workspace populated across every inspector surface (transcript, steps +
/// tool i/o, inbox), symlinked under the lernie workspaces root so the model
/// enumerates it.
struct World {
    _root: TempDir,
    _fx: Fixture,
    model: AppModel,
    state: ShellState,
    ws: PathBuf,
}

fn world() -> World {
    let root = tempdir().unwrap();
    let roots = Roots {
        yog_data: root.path().join("yog"),
        lernie_data: root.path().join("lernie"),
        yog_state: root.path().join("state"),
        balls_clones: root.path().join("balls").join("clones"),
        home: root.path().join("home"),
    };
    std::fs::create_dir_all(roots.lernie_data.join("workspaces")).unwrap();
    std::fs::create_dir_all(&roots.yog_state).unwrap();
    let fx = Fixture::new();
    fx.build_agent("c-1", "hello");
    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();
    let step = fx.path.join("steps/c-1/001");
    std::fs::create_dir_all(step.join("tools/toolu_1")).unwrap();
    // A settled auth-shaped failure (the live stench-pug kind:auth shape) with a
    // usage event: framing Failed → Stopped, tokens still fold, and the §11
    // inline Login banner must reach the paint layer.
    std::fs::write(
        step.join("response.json"),
        b"{\"type\":\"usage\",\"input_tokens\":10,\"output_tokens\":5}\n{\"type\":\"error\",\"kind\":\"auth\",\"message\":\"no credential for this provider\",\"provider_detail\":null}\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();
    fx.deposit_message(
        "c-1",
        "user-001.md",
        "---\nfrom: user\ndeposited_at: t0\n---\nfollow-up message",
    );
    let ws = roots.lernie_data.join("workspaces").join("ws");
    std::os::unix::fs::symlink(&fx.path, &ws).unwrap();
    let balls = Box::new(BlCli::new(Cli::new("bl")));
    let model = AppModel::new(
        roots,
        Some(ws.clone()),
        Arc::new(SystemClock),
        balls,
        Box::new(crate::test_support::OkProbe),
        Some("me".into()),
    );
    let env = Env::from_pairs([("HOME", root.path().display().to_string())]);
    let state = ShellState::new(&env).unwrap();
    World {
        _root: root,
        _fx: fx,
        model,
        state,
        ws,
    }
}

fn input() -> egui::RawInput {
    egui::RawInput {
        screen_rect: Some(egui::Rect::from_min_size(
            egui::Pos2::ZERO,
            egui::vec2(1600.0, 2400.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 the whole window and return every painted galley's text. Two frames:
/// egui panels are their default height on the frame they first appear (the
/// content height lands in panel state for the next frame), so the settled
/// second frame is the one the operator actually sees.
fn painted(world: &mut World, lernie: &Cli, bl: &Cli) -> String {
    // A `bz` handle for the §8.3 Login surface; unspawned here — the Toolchain
    // pane only shells to bz on an explicit click, unreachable headless.
    let bz = Cli::new("bz");
    let ctx = egui::Context::default();
    let _ = ctx.run(input(), |ctx| {
        render(ctx, &mut world.model, &mut world.state, lernie, bl, &bz);
    });
    let out = ctx.run(input(), |ctx| {
        render(ctx, &mut world.model, &mut world.state, lernie, bl, &bz);
    });
    let mut text = String::new();
    for clipped in out.shapes {
        collect_text(&clipped.shape, &mut text);
    }
    text
}

#[test]
fn full_window_reaches_every_data_surface() {
    let (lernie, bl) = (Cli::new("lernie"), Cli::new("bl"));
    let mut world = world();
    let ws = world.ws.clone();
    world.model.focus_agent(&ws, "c-1");
    world.state.inspector.step_sel = Some(0);

    // Altitude 0/1 chrome renders regardless of tab: the attention strip, the
    // tab bar's + mint and overflow (the fixture workspace is foreign), the
    // conversation list with its row, the balls section + Config entry, the
    // composer (message-targeted — an agent is focused), the activity chip,
    // and the budget-spent header.
    world.model.select_tab(InspectorTab::Transcript);
    let base = painted(&mut world, &lernie, &bl);
    assert!(base.contains("need attention"), "attention strip:\n{base}");
    assert!(base.contains('+'), "the tab bar's + mint");
    assert!(base.contains('⋯'), "the foreign/replay overflow menu");
    assert!(base.contains("+ conversation"), "the list's + affordance");
    assert!(base.contains("balls"), "the collapsible balls section");
    assert!(base.contains("⚙ Config"), "Config entry");
    assert!(
        base.contains("→ message c-1"),
        "composer targets the selection"
    );
    assert!(base.contains("Message"), "message verb:\n{base}");
    assert!(base.contains("Scan"), "scan verb");
    assert!(base.contains("activity · 0 ops"), "activity chip:\n{base}");
    assert!(base.contains("c-1"), "conversation row / header");
    assert!(base.contains("budget"), "budget-spent header");
    // The §11 inline Login affordance on the auth-failed latest step — the Z8
    // machinery rendered in the conversation view, verified on the real
    // kind:auth response shape.
    assert!(
        base.contains("failed on credentials"),
        "auth banner:\n{base}"
    );
    assert!(base.contains("Login (bz device flow)"), "login surface");
    // Transcript tab content.
    assert!(
        base.contains("please ping"),
        "transcript delivered:\n{base}"
    );
    assert!(base.contains("pong reply"), "transcript model reply");

    // Steps tab: the list figures and the selected step's tool drill-in.
    world.model.select_tab(InspectorTab::Steps);
    world.state.inspector.step_tab = crate::steps_view::StepTab::Tools;
    let steps = painted(&mut world, &lernie, &bl);
    assert!(steps.contains("tok"), "steps token figure:\n{steps}");
    assert!(steps.contains("toolu_1"), "steps tool i/o");

    // Inbox tab.
    world.model.select_tab(InspectorTab::Inbox);
    assert!(
        painted(&mut world, &lernie, &bl).contains("follow-up message"),
        "inbox deposit"
    );

    // Files tab: the agent worktree listing and the selected file's preview.
    world.model.select_tab(InspectorTab::Files);
    world.state.inspector.files_sel = Some(0);
    let files = painted(&mut world, &lernie, &bl);
    assert!(
        files.contains("goal.md"),
        "files worktree listing:\n{files}"
    );
    assert!(files.contains("hello"), "goal.md preview:\n{files}");

    // Config tab: the governing config frozen label.
    world.model.select_tab(InspectorTab::Config);
    assert!(
        painted(&mut world, &lernie, &bl).contains("policy frozen at"),
        "governing config"
    );

    // Config mode: the three editors and the workspace's config branch listing.
    world.state.config.active = true;
    let editors = painted(&mut world, &lernie, &bl);
    assert!(
        editors.contains("brazen config.toml"),
        "brazen editor:\n{editors}"
    );
    assert!(
        editors.contains("lernie global config"),
        "lernie-global editor"
    );
    assert!(
        editors.contains("workspace config branches"),
        "config-branch editor"
    );
    assert!(editors.contains("config/default"), "config branch listing");
}