Files
nisse/nisse-dev/tests/test_execution.py
T
dongarandClaude Opus 5.5 9968a0922a
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 2m59s
CI / e2e (pull_request) Successful in 10m33s
M4.1: toolchain preflight
A model works out and sets up what a project needs on its build VM;
code makes sure that happens, bounds it and checks the result (D3).

- Before each driver run on a project, one round trip checks the build
  VM: the harness's own baseline (missing: the item waits, a template
  problem), the workspace clone (missing: cloned again and said; the
  item resumes from what it pushed) and a per-project marker.
- No marker (a new project, a new or wiped VM): the item's driver model
  runs a bounded preflight first, with its own prompt, the execution
  tools (screened as ever) and toolchain_ready. The harness runs each
  reported version command itself (version-shaped commands only, even
  from the marker, which the model's commands could edit) and writes
  the marker. Gaps and an unfinished preflight are facts and work goes
  on; the thread hears only of installs and gaps. A rebuilt VM starts
  from the last toolchain.
- Later runs check the versions again; a change is a fact and a thread
  line. The PR body says what the work was tested with.
- A changed host key is its own failure (HostKeyChanged): the item
  waits for the operator's `trust new host key`, which drops the pin;
  then the VM is set up again.
- A prompt line: install what the work needs on the build VM, say why,
  and put lasting needs in the project's own pins.
- tuning.preflight: max_turns, command_timeout_s.
- The property walk gains a VM wipe and ready_before_work (checked
  against a mutant that skips preflight); parked_work_safe accepts that
  a wipe loses what wasn't pushed (D3).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
2026-10-08 15:43:30 +00:00

109 lines
3.9 KiB
Python

"""Running commands on a tool VM over SSH (L13), against a throwaway sshd."""
import os
from pathlib import Path
import pytest
from nisse_dev.execution import REDACTED, HostUnavailable, SSHHost, redact
def test_exit_codes_and_streams_come_back(ssh_host: SSHHost) -> None:
result = ssh_host.execute("echo out; echo err >&2; exit 3")
assert (result.exit_code, result.stdout, result.stderr) == (3, "out\n", "err\n")
assert not result.ok
def test_cwd_and_stdin(ssh_host: SSHHost, tmp_path: Path) -> None:
result = ssh_host.execute("pwd; cat", cwd=str(tmp_path), stdin="line one\nline two")
assert result.stdout == f"{tmp_path}\nline one\nline two\n"
def test_stdin_that_looks_like_a_delimiter_survives(ssh_host: SSHHost) -> None:
payload = "NISSE_EOF\nEOF\n'quoted' $notexpanded `nor this`"
assert ssh_host.execute("cat", stdin=payload).stdout == payload + "\n"
def test_without_stdin_a_command_cannot_read_the_script(ssh_host: SSHHost) -> None:
# The script arrives on the same channel; the command must see /dev/null.
result = ssh_host.execute("cat; echo done")
assert result.stdout == "done\n"
def test_a_missing_cwd_fails_before_running(ssh_host: SSHHost, tmp_path: Path) -> None:
marker = tmp_path / "ran"
result = ssh_host.execute(f"touch {marker}", cwd=str(tmp_path / "absent"))
assert result.exit_code == 125
assert not marker.exists()
def test_a_command_exiting_255_is_not_mistaken_for_ssh(ssh_host: SSHHost) -> None:
assert ssh_host.execute("exit 255").exit_code == 254
def test_an_unreachable_host_raises(tmp_path: Path) -> None:
host = SSHHost("127.0.0.1", key=tmp_path / "k", state_dir=tmp_path / "s", port=1)
with pytest.raises(HostUnavailable):
host.execute("true")
def test_a_timeout_is_reported(ssh_host: SSHHost) -> None:
result = ssh_host.execute("sleep 30", timeout_s=1)
assert result.timed_out
def test_secrets_reach_the_command_and_never_its_output(ssh_host: SSHHost) -> None:
value = "s-" + os.urandom(12).hex()
result = ssh_host.execute(
'echo "$API_KEY"; echo "$API_KEY" >&2', secrets={"API_KEY": value}
)
assert result.ok
assert value not in result.stdout + result.stderr
assert result.stdout == f"{REDACTED}\n"
def test_secrets_are_never_on_a_command_line(ssh_host: SSHHost, tmp_path: Path) -> None:
# Made up here, so no command line in this test run can contain it except
# by leaking it.
value = "s-" + os.urandom(12).hex()
seen = tmp_path / "ps.txt"
ssh_host.execute(f"ps -eo args > {seen}", secrets={"API_KEY": value})
assert "bash -s" in seen.read_text() # the listing did include our shell
assert value not in seen.read_text()
def test_redact_masks_longest_values_first() -> None:
assert redact("abc-abcdef", ["abcdef", "abc-abcdef"]) == REDACTED
assert redact("short ab stays", ["ab"]) == "short ab stays" # too short to mask
def test_a_changed_host_key_is_its_own_failure_and_only_forgetting_clears_it(
ssh_host: SSHHost, tmp_path: Path
) -> None:
import subprocess
from nisse_dev.execution import HostKeyChanged
host = SSHHost(
ssh_host.address,
key=ssh_host._key,
user=ssh_host.user,
port=ssh_host.port,
state_dir=tmp_path / "state",
)
assert host.execute("true").ok # first contact pins the key
host.close()
# Someone else's key where the pinned one was: a rebuilt VM, or not it.
subprocess.run(
["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(tmp_path / "other")],
check=True,
)
other = (tmp_path / "other.pub").read_text().split()
known = tmp_path / "state" / "known_hosts"
name = host.address if host.port == 22 else f"[{host.address}]:{host.port}"
known.write_text(f"{name} {other[0]} {other[1]}\n")
with pytest.raises(HostKeyChanged):
host.execute("true")
host.forget_host_key()
assert host.execute("true").ok # the real key pinned again