#!/usr/bin/env python3 """A session runs on one of two machines, and is told about that one only. python3 tests/test_vm_two_machines.py WHY Ubuntu or Windows. The differences are not decoration: the project folder is `/mnt/project` or `Z:\\`, and the shell is bash or Windows PowerShell 5.1, which has no `&&`. There used to be four kinds of session - those two machines by cli/gui - and the second axis is gone. The calling model chose it and chose wrong: asked to build a GUI application it picked the command line twice, with the rule in the tool docstring and in the system prompt both times. A session now gets the command line, the lookup suite AND the screen, and decides for itself what it needs. Both machines used to get the same prompt and the same tool descriptions, which named both and left the agent to work out which half was about it. It does not work out: this stack has repeatedly proved the model treats tool text as fact rather than as something to reason around. Measured, and the reason this file exists: a WINDOWS session wrote itself the task *"check /mnt/project accessibility"* - the Linux path - because the text in front of it said that was where work survives and nothing said otherwise. It cost a step, and it would have cost the work if that path had been writable. """ import os import sys import types HERE = os.path.dirname(os.path.abspath(__file__)) APP = os.path.join(HERE, "..", "vmagent", "app") sys.path.insert(0, APP) sys.path.insert(0, os.path.join(HERE, "..", "vm", "vmd")) FAILS: list[str] = [] def check(label: str, condition: bool, detail: str = "") -> None: if condition: print(f"ok {label}") else: FAILS.append(label) print(f"FAIL {label}\n {detail}") import tools # noqa: E402 import loop # noqa: E402 MACHINES = ("ubuntu", "windows") OTHER = {"ubuntu": "Z:\\", "windows": "/mnt/project"} OWN = {"ubuntu": "/mnt/project", "windows": "Z:\\"} # --- the prompt ---------------------------------------------------------- for os_name in MACHINES: prompt = loop._load_prompt(os_name) check(f"{os_name}: the prompt names its own project folder", OWN[os_name] in prompt, os_name) check(f"{os_name}: and never the other machine's", OTHER[os_name] not in prompt, "this is the exact text that put /mnt/project in a Windows task list") check(f"{os_name}: every session is told it has a screen", "There is a screen" in prompt) check(f"{os_name}: and told that most work does not need it", "Most work does not need any of that" in prompt, "the screen is the exception; a command is faster and more reliable") windows = loop._load_prompt("windows") check("a Windows session is warned that 5.1 has no && or ||", "&&" in windows and "5.1" in windows, "this cost a session: the supervisor's own wrapper hit it too") check("an Ubuntu session is not given PowerShell trivia", "PowerShell" not in loop._load_prompt("ubuntu")) # A fragment that is not there must not stop a session starting. missing = loop._load_prompt("plan9") check("an unknown machine still produces a usable prompt", len(missing) > 1000 and "There is a screen" in missing, "refusing to start over a missing file would be the worse failure") # --- the tool descriptions ---------------------------------------------- for os_name in MACHINES: schemas = tools.schemas(os_name) names = [t["function"]["name"] for t in schemas] text = " ".join(t["function"]["description"] for t in schemas) tag = os_name check(f"{tag}: the tools name its own project folder", OWN[os_name] in text) check(f"{tag}: and not the other machine's", OTHER[os_name] not in text, tag) check(f"{tag}: the screen tools are always there", {"screenshot", "click", "launch_app"} <= set(names)) check(f"{tag}: so is the lookup suite", "lookup_cve" in names and "web_search" in names, "a session that cannot cheaply look something up reasons about it instead") run_win = next(t for t in tools.schemas("windows") if t["function"]["name"] == "run")["function"]["description"] run_ubu = next(t for t in tools.schemas("ubuntu") if t["function"]["name"] == "run")["function"]["description"] check("the Windows run tool says PowerShell, and says it has no &&", "PowerShell" in run_win and "&&" in run_win) check("the Ubuntu run tool says bash, and mentions neither", "bash" in run_ubu and "PowerShell" not in run_ubu and "&&" not in run_ubu) check("neither describes the machine the session is not in", "bash" not in run_win and "Z:" not in run_ubu) # --- the command feed in a GUI session ---------------------------------- # # Every tool a GUI session uses is a desktop verb. None of them rendered, # so the feed under the screen would have been ten blank rows for ever. for verb, args in (("screenshot", {}), ("click", {"x": 4, "y": 5}), ("key", {"name": "Return"}), ("type", {"text": "x"}), ("scroll", {"x": 1, "y": 2, "amount": -3}), ("move", {"x": 1, "y": 2})): check(f"a GUI session's {verb} shows in the command feed", loop._terminal_for(verb, args) != [], "the third box would be blank rows for the whole session") check("a click says where it went", "4" in loop._terminal_for("click", {"x": 4, "y": 5})[0] and "5" in loop._terminal_for("click", {"x": 4, "y": 5})[0]) # --- putting something on the real desktop ------------------------------- # # The mechanism for this lived inside open_page and nowhere else, welded # to browsers. A session that built its own GUI application had to # rediscover it, and one did: it wrote a working 259-line WinForms toolkit # in nine steps and then spent the remaining twenty of its sixty trying to # get a window on screen, and ran out. for os_name in MACHINES: names = [t["function"]["name"] for t in tools.schemas(os_name)] check(f"{os_name}: a session can launch a program onto the desktop", "launch_app" in names) launch = next(t for t in tools.schemas("windows") if t["function"]["name"] == "launch_app")["function"]["description"] check("it says why run is the wrong way to start a window", "nobody is looking" in launch, "starting a GUI from run does not fail, it just never appears") check("and that a screenshot is still what proves it appeared", "screenshot" in launch) browser_src = open(os.path.join(HERE, "..", "vm", "vmd", "browser.py"), encoding="utf-8").read() check("no launcher passes env, because that is what removed PATH", "env=[" not in browser_src and "OWUIVM_" not in browser_src, "passing env REPLACES the child environment: schtasks then cannot be found") check("and the check excludes the launcher's own process", "$PID" in browser_src, "otherwise it matches itself and reports a program that never started") # Passing `env` to guest-exec REPLACES the environment, so PATH is gone # and a bare "powershell" cannot be resolved. Measured: every launch died # with "Failed to execute child process (No such file or directory)", # including notepad.exe. Latent in open_page on Windows since it was # written, because `run` passes no env and so never showed it. check("the Windows interpreter is named in full wherever env is passed", '"powershell", "-NoProfile"' not in browser_src and "WINDOWS_POWERSHELL" in browser_src, "a bare name needs a PATH, and passing env takes PATH away") check("and the launch is confirmed by the process still being alive", "exited" in browser_src and "93" in browser_src, "a GUI program that dies on a startup error also returns cleanly") # A desktop session needs more turns: look, wait, click, look again. loop_src = open(os.path.join(HERE, "..", "vmagent", "app", "loop.py"), encoding="utf-8").read() check("there is one step budget and it allows for looking at a screen", "GUI_MAX_STEPS" not in loop_src and 'MAX_STEPS", "110"' in loop_src, "every session can reach for the screen, so every session gets the room") print() if FAILS: print(f"FAILED {len(FAILS)}") raise SystemExit(1) print("each machine is described to itself, and every session has the whole toolbox")