newt-core 0.8.0

Newt-Agent core types, errors, and the NeMoCode-style tier router
use super::super::scheduled::{execute_update_plan, plan_block, SessionStepLedger};
use super::*;

fn selection(path: &str) -> Value {
    json!({"path":path,"scope":"repository implementation files",
        "evidence":"inventory ranked this source first","revision":"Initial selection"})
}
fn pin(ledger: &dyn StepLedger, path: &str) {
    let out = execute_update_plan(
        &json!({"plan":[{"step":"Extract"},{"step":"Check"}],
        "target":selection(path)}),
        ledger,
        false,
        0,
    );
    assert!(!out.starts_with("error:"), "{out}");
}

#[test]
fn selected_target_revision_is_explicit_linked_and_omission_preserves_it() {
    let ledger = SessionStepLedger::default();
    pin(&ledger, "alpha/nested/index.py");
    let before = ledger.snapshot().target.unwrap();
    let mut revised = selection("beta/nested/entry.ts");
    revised.as_object_mut().unwrap().remove("revision");
    let err = execute_update_plan(
        &json!({"plan":["Replan"],"target":revised}),
        &ledger,
        false,
        0,
    );
    assert!(err.starts_with("error:"), "{err}");
    assert_eq!(ledger.snapshot().target.as_ref(), Some(&before));
    revised["revision"] = json!("Operator changed scope to beta; fresh inventory");
    let out = execute_update_plan(
        &json!({"plan":["Replan"],"target":revised}),
        &ledger,
        false,
        0,
    );
    assert!(out.contains("beta/nested/entry.ts"), "{out}");
    let after = ledger.snapshot().target.unwrap();
    assert_ne!(after.id, before.id);
    assert_eq!(after.selection.previous, Some(before.id));
    execute_update_plan(&json!({"plan":["Finish"]}), &ledger, false, 0);
    assert_eq!(ledger.snapshot().target, Some(after));
    ledger.clear();
    assert!(ledger.snapshot().target.is_none());
}

#[test]
fn selected_target_paths_are_portable_and_identity_is_verified() {
    for path in [
        "../escape",
        "/absolute",
        "C:/drive",
        "a\\b",
        "a//b",
        "./a",
        "a/../b",
        "a\nb",
    ] {
        assert!(
            PinnedTarget::revise(&selection(path), None).is_err(),
            "{path}"
        );
    }
    for path in [
        "a/nested/index.py",
        "engine/module/root.code",
        "single-file",
        "目录/文件.txt",
    ] {
        assert!(PinnedTarget::revise(&selection(path), None)
            .unwrap()
            .valid());
    }
    let mut target = PinnedTarget::revise(&selection("a/index.py"), None).unwrap();
    target.selection.path = "wrong/index.py".into();
    assert!(!target.valid());
    assert!(target.label().contains("INVALID PIN"));
    assert!(card(&target, Some(&["wrong/index.py".into()])).contains("identity mismatch"));
}

#[test]
fn selected_target_wrong_crate_enoent_does_not_reselect_and_diff_is_advisory() {
    let target = PinnedTarget::revise(&selection("engine/nested/root.code"), None).unwrap();
    let outside = vec!["cli/nested/root.code".into()];
    let warning = card(&target, Some(&outside));
    assert!(warning.contains("work landed outside the selected source"));
    assert!(warning.contains("engine/nested/root.code"));
    assert!(warning.contains("ENOENT"));
    assert!(card(&target, None).contains("unverified, not a pass"));
    assert!(card(&target, Some(&[])).contains("No task diff"));
    assert!(!card(
        &target,
        Some(&[target.selection.path.clone(), "new/component.code".into()])
    )
    .contains("work landed outside"));
}

#[test]
fn selected_target_projection_survives_compaction_continue_and_done_plan() {
    let ledger = SessionStepLedger::default();
    pin(&ledger, "arbitrary/nested/root.xyz");
    execute_update_plan(
        &json!({"plan":[{"step":"Done","status":"completed"}]}),
        &ledger,
        false,
        0,
    );
    let mut messages = vec![json!({"role":"user","content":"continue"})];
    super::super::prompt_read::ensure_active_prompt_card(
        &mut messages,
        super::super::prompt_read::PromptReadContext::new(None, "Original task", None),
        None,
    );
    let mut projection = Projection::default();
    projection.refresh(
        &mut messages,
        Some(&ledger),
        ".",
        &crate::Scope::none(),
        None,
        false,
    );
    for n in 0..20 {
        messages.push(json!({"role":"user","content":format!("noise {n}")}));
    }
    let head = super::super::trim::protected_prompt_head_len(
        &messages,
        super::super::prompt_read::ACTIVE_PROMPT_PREFIX,
    );
    let mut trimmed = super::super::trim::trim_for_summary(&messages, head, 2);
    assert!(trimmed.iter().any(|m| m["content"]
        .as_str()
        .is_some_and(|s| s.contains("arbitrary/nested/root.xyz"))));
    projection.refresh(
        &mut trimmed,
        Some(&ledger),
        ".",
        &crate::Scope::none(),
        None,
        false,
    );
    assert_eq!(
        trimmed
            .iter()
            .filter(|m| m["content"].as_str().is_some_and(|s| s.starts_with(PREFIX)))
            .count(),
        1
    );
    assert!(plan_block(&ledger)
        .unwrap()
        .contains("arbitrary/nested/root.xyz"));
    // Responses compaction temporarily reconstructs the same protected head.
    let mut input = vec![json!({"role":"user","content":"continue"})];
    Projection::default().refresh(
        &mut input,
        Some(&ledger),
        ".",
        &crate::Scope::none(),
        None,
        true,
    );
    let protected =
        super::super::compress::protect_active_prompt_for_compression(&input, "Original task");
    let head = super::super::trim::protected_prompt_head_len(
        &protected,
        super::super::prompt_read::ACTIVE_PROMPT_PREFIX,
    );
    assert!(protected[..head]
        .iter()
        .any(|m| m["content"].as_str().is_some_and(|s| s.starts_with(PREFIX))));
}

/// Grounds the pure path reconciliation in real local Git, with no network.
#[tokio::test]
async fn selected_target_git_diff_includes_commits_dirty_untracked_and_source_rename() {
    use crate::agentic::tools::disable_ocap_tests::{env_lock, EnvVar};
    let _lock = env_lock().await;
    let empty = tempfile::NamedTempFile::new().unwrap();
    let _global = EnvVar::set("GIT_CONFIG_GLOBAL", empty.path().to_str().unwrap());
    let _system = EnvVar::set("GIT_CONFIG_NOSYSTEM", "1");
    let _count = EnvVar::set("GIT_CONFIG_COUNT", "0");
    let _parameters = EnvVar::set("GIT_CONFIG_PARAMETERS", "");

    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();
    let run = |args: &[&str]| {
        let out = crate::agentic::tools::git_fixture::hermetic_git(root, root)
            .args(args)
            .output()
            .unwrap();
        assert!(
            out.status.success(),
            "{}",
            String::from_utf8_lossy(&out.stderr)
        );
    };
    run(&["init", "-q"]);
    run(&["config", "user.name", "Fixture"]);
    run(&["config", "user.email", "fixture@example.invalid"]);
    std::fs::create_dir_all(root.join("nested")).unwrap();
    std::fs::write(root.join("nested/source.txt"), "original").unwrap();
    std::fs::write(root.join("other.txt"), "other").unwrap();
    run(&["add", "."]);
    run(&["-c", "commit.gpgsign=false", "commit", "-qm", "base"]);
    let mut target = PinnedTarget::revise(&selection("nested/source.txt"), None).unwrap();
    target
        .bind_baseline(baseline(root, &crate::Scope::All))
        .unwrap();
    assert_eq!(diff_paths(&target, root, &crate::Scope::All), Some(vec![]));
    std::fs::write(root.join("other.txt"), "changed").unwrap();
    assert_eq!(
        diff_paths(&target, root, &crate::Scope::All),
        Some(vec!["other.txt".into()])
    );
    run(&["add", "."]);
    assert_eq!(
        diff_paths(&target, root, &crate::Scope::All),
        Some(vec!["other.txt".into()])
    );
    run(&["-c", "commit.gpgsign=false", "commit", "-qm", "outside"]);
    let paths = diff_paths(&target, root, &crate::Scope::All).unwrap();
    assert!(card(&target, Some(&paths)).contains("work landed outside"));
    std::fs::write(root.join("untracked.txt"), "new").unwrap();
    assert!(diff_paths(&target, root, &crate::Scope::All)
        .unwrap()
        .contains(&"untracked.txt".into()));
    run(&["mv", "nested/source.txt", "nested/moved.txt"]);
    let paths = diff_paths(&target, root, &crate::Scope::All).unwrap();
    assert!(paths.contains(&"nested/source.txt".into()));
    assert!(!card(&target, Some(&paths)).contains("work landed outside"));
    assert!(diff_paths(&target, root, &crate::Scope::none()).is_none());
}

/// PR #2844: read-only projection must never execute repository conversion
/// programs, even when Git would need to rehash a dirty tracked file.
#[test]
fn selected_target_projection_never_executes_clean_or_process_filters() {
    for kind in ["clean", "process"] {
        let temp = tempfile::tempdir().unwrap();
        let root = temp.path().join("repo");
        std::fs::create_dir(&root).unwrap();
        let run = |args: &[&str]| {
            let out = crate::agentic::tools::git_fixture::hermetic_git(&root, &root)
                .args(args)
                .output()
                .unwrap();
            assert!(
                out.status.success(),
                "{}",
                String::from_utf8_lossy(&out.stderr)
            );
        };
        run(&["init", "-q"]);
        run(&["config", "user.name", "Fixture"]);
        run(&["config", "user.email", "fixture@example.invalid"]);
        std::fs::write(root.join("source.txt"), "original\n").unwrap();
        run(&["add", "."]);
        run(&["-c", "commit.gpgsign=false", "commit", "-qm", "base"]);
        let ledger = SessionStepLedger::default();
        pin(&ledger, "source.txt");
        let mut snapshot = ledger.snapshot();
        snapshot
            .target
            .as_mut()
            .unwrap()
            .bind_baseline(baseline(&root, &crate::Scope::All))
            .unwrap();
        ledger.restore(&snapshot);
        run(&[
            "config",
            &format!("filter.hostile.{kind}"),
            "echo ran > ../sentinel; exit 1",
        ]);
        std::fs::write(root.join(".gitattributes"), "source.txt filter=hostile\n").unwrap();
        std::fs::write(root.join("source.txt"), "changed contents require rehash\n").unwrap();
        let mut messages = Vec::new();
        Projection::default().refresh(
            &mut messages,
            Some(&ledger),
            root.to_str().unwrap(),
            &crate::Scope::All,
            None,
            false,
        );
        assert!(
            !temp.path().join("sentinel").exists(),
            "{kind} executed on host"
        );
        assert!(messages[0]["content"]
            .as_str()
            .unwrap()
            .contains("unverified, not a pass"));
    }
}