Initial Commit
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
#!/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")
|
||||
Reference in New Issue
Block a user