harn-cli 0.10.54

CLI for the Harn programming language — run, test, REPL, format, and lint
Documentation
//! Plan-level capability tests: what the solver decides is required, before
//! any edit is emitted.

use super::*;

/// A capability reached only through `${...}` is still reached.
///
/// `visit::walk_program` stops at `Node::InterpolatedString`: the lexer keeps a
/// hole as unparsed source text, so it has no AST children. The whole-program
/// solver computed required capabilities with that walk, so a handle used only
/// inside a hole looked unused. The repair then deleted it from the parameter
/// type and every call site while the interpolation kept calling it. `harn fix
/// --apply` runs unattended in the fleet bump workflow, so it rewrote working
/// helpers into code that does not compile and shipped that as a bump PR
/// (harn-cloud#1469).
///
/// The fixture needs THREE capabilities. With two, no attenuation repair is
/// proposed at all and the test passes vacuously — which is exactly how an
/// earlier version of it passed against the unfixed solver.
///
/// The assertion is EQUIVALENCE, not merely "did not delete": the same use
/// inside and outside a hole must plan the same repair. "Did not delete" alone
/// would also hold if the analysis simply gave up on any file containing a hole.
#[test]
fn capability_plan_counts_uses_inside_string_interpolation() {
    let plan_edits = |name: &str, body: &str| {
        let temp = tempfile::TempDir::new().unwrap();
        let script = temp.path().join(name);
        fs::write(
            &script,
            format!(
                "fn with_temp_dir(harness: {{fs: HarnessFs, process: HarnessProcess, random: HarnessRandom}}, body) {{\n\
                 {body}\
                 \x20 harness.fs.mkdir(dir)\n\
                 \x20 const outcome = try {{\n\
                 \x20   body(dir)\n\
                 \x20 }}\n\
                 \x20 harness.process.run({{program: \"rm\", args: [\"-rf\", dir]}})\n\
                 \x20 return unwrap(outcome)\n\
                 }}\n\n\
                 pipeline main(harness: Harness, task) {{\n\
                 \x20 return with_temp_dir(\n\
                 \x20   {{fs: harness.fs, process: harness.process, random: harness.random}},\n\
                 \x20   {{ dir -> dir }},\n\
                 \x20 )\n\
                 }}\n"
            ),
        )
        .unwrap();
        // `--apply` is a different entry from `build_plan`; only this one
        // reaches the repair that broke. Apply for real and read the file back,
        // so the assertion is on emitted code rather than on a plan.
        super::apply_repairs_with_options(
            temp.path(),
            RepairSafety::SurfaceChanging,
            false,
            super::FixOptions::capability_migrations(),
        )
        .unwrap();
        fs::read_to_string(&script).unwrap()
    };

    // Identical programs; `random` is reached through a hole in one and through
    // a plain statement in the other.
    let through_hole = plan_edits(
        "hole.harn",
        "  const dir = \".tmp-${harness.random.uuid_v7()}\"\n",
    );
    let through_statement = plan_edits(
        "statement.harn",
        "  const id = harness.random.uuid_v7()\n  const dir = \".tmp-${id}\"\n",
    );

    let signature = |source: &str| {
        source
            .lines()
            .find(|line| line.starts_with("fn with_temp_dir"))
            .unwrap_or_default()
            .to_string()
    };

    assert!(
        !through_hole.contains("harness.random")
            || signature(&through_hole).contains("HarnessRandom")
            || signature(&through_hole).contains("harness: Harness"),
        "the body still calls `harness.random`, so the repair must not have removed it \
         from the signature:\n{through_hole}"
    );
    assert_eq!(
        signature(&through_hole),
        signature(&through_statement),
        "a capability reached through `${{...}}` must be repaired the same as one \
         reached through a statement"
    );
}

#[test]
fn capability_plan_repairs_imported_helpers_without_type_diagnostics() {
    let temp = tempfile::TempDir::new().unwrap();
    let script = temp.path().join("main.harn");
    fs::write(
        &script,
        "import { agent_capture_events } from \"std/agent/events\"\nimport { agent_parse_tool_calls } from \"std/agent/primitives\"\nimport { agent_session_finalize, agent_session_messages, agent_reminder_providers_fire } from \"std/agent/state\"\n\nfn custom_agent(harness: Harness) -> HarnessAgent {\n  return harness.agent\n}\n\npipeline main(harness: Harness, task) {\n  const session = \"session\"\n  const messages = agent_session_messages(session)\n  agent_session_finalize(session, \"done\")\n  agent_session_finalize(custom_agent(harness), session, \"already explicit\")\n  const captured = agent_capture_events(session, fn() { nil })\n  const parsed = agent_parse_tool_calls(\"<tool_call>x({})</tool_call>\", [], \"text\")\n  const report = agent_reminder_providers_fire(session, \"session_idle\", {}, {})\n  return {messages: messages, captured: captured, parsed: parsed, report: report}\n}\n",
    )
    .unwrap();
    let files = vec![script.clone()];
    let graph = commands::check::build_module_graph(&files);

    let repairs = whole_program_capabilities::plan(&files, &graph, &[], &BTreeSet::new()).unwrap();

    assert_eq!(
        repairs
            .iter()
            .flat_map(|repair| &repair.edits)
            .filter(|edit| {
                edit.span.start == edit.span.end && edit.replacement == "harness.agent, "
            })
            .count(),
        5,
        "every imported Agent helper must derive its prefix from the module signature: {repairs:#?}"
    );

    let mut updated = fs::read_to_string(&script).unwrap();
    let mut edits = repairs
        .iter()
        .flat_map(|repair| repair.edits.iter().cloned())
        .collect::<Vec<_>>();
    edits.sort_by_key(|edit| std::cmp::Reverse((edit.span.start, edit.span.end)));
    for edit in edits {
        updated.replace_range(edit.span.start..edit.span.end, &edit.replacement);
    }
    fs::write(&script, updated).unwrap();
    let repaired_graph = commands::check::build_module_graph(&files);
    let fixed_point =
        whole_program_capabilities::plan(&files, &repaired_graph, &[], &BTreeSet::new()).unwrap();
    assert!(
        fixed_point.is_empty(),
        "already-migrated imported calls must be a planner fixed point: {fixed_point:#?}"
    );
}

#[test]
fn capability_plan_preserves_explicit_imported_capability_expressions() {
    let temp = tempfile::TempDir::new().unwrap();
    let script = temp.path().join("main.harn");
    fs::write(
        &script,
        "import { terminal_width } from \"std/tui\"\n\nfn custom_term(term: HarnessTerm) -> HarnessTerm {\n  return term\n}\n\npipeline main(harness: Harness, task) {\n  const term = custom_term(harness.term)\n  return [terminal_width(custom_term(harness.term)), terminal_width(term)]\n}\n",
    )
    .unwrap();
    let files = vec![script];
    let graph = commands::check::build_module_graph(&files);

    let repairs = whole_program_capabilities::plan(&files, &graph, &[], &BTreeSet::new()).unwrap();

    assert!(
        repairs.is_empty(),
        "explicit capability expressions must not be shifted into optional ordinary parameters: {repairs:#?}"
    );
}

#[test]
fn capability_plan_preserves_an_unknown_capability_identifier_when_ordinary_args_are_missing() {
    let temp = tempfile::TempDir::new().unwrap();
    let script = temp.path().join("main.harn");
    fs::write(
        &script,
        "import { agent_session_messages } from \"std/agent/state\"\n\nfn custom_agent(harness: Harness) -> HarnessAgent {\n  return harness.agent\n}\n\npipeline main(harness: Harness, task) {\n  const agent = custom_agent(harness)\n  return agent_session_messages(agent)\n}\n",
    )
    .unwrap();
    let files = vec![script];
    let graph = commands::check::build_module_graph(&files);

    let repairs = whole_program_capabilities::plan(&files, &graph, &[], &BTreeSet::new()).unwrap();

    assert!(
        !repairs
            .iter()
            .flat_map(|repair| &repair.edits)
            .any(|edit| edit.replacement == "harness.agent, "),
        "an unknown identifier may already be the carrier; adding one would shift it into the missing session slot: {repairs:#?}"
    );
}

#[test]
fn capability_plan_uses_inferred_capability_types_to_repair_a_different_missing_carrier() {
    let temp = tempfile::TempDir::new().unwrap();
    let library = temp.path().join("library.harn");
    let entrypoint = temp.path().join("main.harn");
    fs::write(
        &library,
        "pub fn run_with_root(harness: Harness, agent: HarnessAgent) {\n  return {root: harness, agent: agent}\n}\n",
    )
    .unwrap();
    fs::write(
        &entrypoint,
        "import { run_with_root } from \"./library\"\n\nfn custom_agent(harness: Harness) -> HarnessAgent {\n  return harness.agent\n}\n\npipeline main(harness: Harness, task) {\n  const agent = custom_agent(harness)\n  return run_with_root(agent)\n}\n",
    )
    .unwrap();
    let files = vec![entrypoint, library];
    let graph = commands::check::build_module_graph(&files);

    let repairs = whole_program_capabilities::plan(&files, &graph, &[], &BTreeSet::new()).unwrap();

    assert!(
        repairs
            .iter()
            .flat_map(|repair| &repair.edits)
            .any(|edit| edit.replacement == "harness, "),
        "an inferred Agent cannot occupy a root Harness slot, so the missing root carrier is observable: {repairs:#?}"
    );
}

#[test]
fn capability_plan_disambiguates_shadowed_inferred_bindings_by_declaration() {
    let temp = tempfile::TempDir::new().unwrap();
    let script = temp.path().join("main.harn");
    fs::write(
        &script,
        "import { agent_session_messages } from \"std/agent/state\"\n\nfn custom_agent(harness: Harness) -> HarnessAgent {\n  return harness.agent\n}\n\npipeline main(harness: Harness, task) {\n  const value = custom_agent(harness)\n  const before = agent_session_messages(value, \"outer-before\")\n  const nested = [1].map({ _ ->\n    const value = \"nested-session\"\n    return agent_session_messages(value)\n  })\n  const after = agent_session_messages(value, \"outer-after\")\n  return {before: before, nested: nested, after: after}\n}\n",
    )
    .unwrap();
    let files = vec![script];
    let graph = commands::check::build_module_graph(&files);

    let repairs = whole_program_capabilities::plan(&files, &graph, &[], &BTreeSet::new()).unwrap();

    assert_eq!(
        repairs
            .iter()
            .flat_map(|repair| &repair.edits)
            .filter(|edit| edit.replacement == "harness.agent, ")
            .count(),
        1,
        "only the string-valued shadow should receive the missing Agent carrier: {repairs:#?}"
    );
}

#[test]
fn capability_plan_completes_a_partial_imported_capability_prefix() {
    let temp = tempfile::TempDir::new().unwrap();
    let script = temp.path().join("main.harn");
    fs::write(
        &script,
        "import { which } from \"std/os\"\n\npipeline main(harness: Harness, task) {\n  return which(harness.tools, \"git\")\n}\n",
    )
    .unwrap();
    let files = vec![script.clone()];
    let graph = commands::check::build_module_graph(&files);

    let repairs = whole_program_capabilities::plan(&files, &graph, &[], &BTreeSet::new()).unwrap();
    let edits = repairs
        .iter()
        .flat_map(|repair| &repair.edits)
        .filter(|edit| edit.replacement == ", harness.system")
        .collect::<Vec<_>>();
    assert_eq!(
        edits.len(),
        1,
        "only the missing capability suffix should be inserted: {repairs:#?}"
    );
    assert_eq!(
        &fs::read_to_string(&script).unwrap()[edits[0].span.start..edits[0].span.end],
        ""
    );

    let mut updated = fs::read_to_string(&script).unwrap();
    let mut all_edits = repairs
        .iter()
        .flat_map(|repair| repair.edits.iter().cloned())
        .collect::<Vec<_>>();
    all_edits.sort_by_key(|edit| std::cmp::Reverse((edit.span.start, edit.span.end)));
    for edit in all_edits {
        updated.replace_range(edit.span.start..edit.span.end, &edit.replacement);
    }
    assert_eq!(
        call_argument_paths(&updated, "which")[0],
        [
            Some("harness.tools".to_string()),
            Some("harness.system".to_string()),
            None,
        ]
    );
    fs::write(&script, updated).unwrap();
    let repaired_graph = commands::check::build_module_graph(&files);
    assert!(
        whole_program_capabilities::plan(&files, &repaired_graph, &[], &BTreeSet::new())
            .unwrap()
            .is_empty(),
        "a completed imported prefix must be a planner fixed point"
    );
}

#[test]
fn capability_plan_resolves_private_imported_capability_aliases() {
    let temp = tempfile::TempDir::new().unwrap();
    let library = temp.path().join("library.harn");
    let entrypoint = temp.path().join("main.harn");
    fs::write(
        &library,
        "type AgentHandle = HarnessAgent\n\npub fn imported_helper(agent: AgentHandle, session: string) {\n  return agent.snapshot(session)\n}\n",
    )
    .unwrap();
    fs::write(
        &entrypoint,
        "import { imported_helper } from \"./library\"\n\npipeline main(harness: Harness, task) {\n  return imported_helper(\"session\")\n}\n",
    )
    .unwrap();
    let files = vec![entrypoint, library];
    let graph = commands::check::build_module_graph(&files);

    let repairs = whole_program_capabilities::plan(&files, &graph, &[], &BTreeSet::new()).unwrap();

    assert!(
        repairs.iter().flat_map(|repair| &repair.edits).any(|edit| {
            edit.span.start == edit.span.end && edit.replacement == "harness.agent, "
        }),
        "a private signature alias must resolve through the module graph: {repairs:#?}"
    );
}