quality-loop 0.4.1__tar.gz → 0.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {quality_loop-0.4.1/quality_loop.egg-info → quality_loop-0.4.2}/PKG-INFO +1 -1
  2. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/__init__.py +1 -1
  3. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/guest/__init__.py +36 -4
  4. {quality_loop-0.4.1 → quality_loop-0.4.2/quality_loop.egg-info}/PKG-INFO +1 -1
  5. {quality_loop-0.4.1 → quality_loop-0.4.2}/tests/test_guest.py +24 -0
  6. {quality_loop-0.4.1 → quality_loop-0.4.2}/LICENSE +0 -0
  7. {quality_loop-0.4.1 → quality_loop-0.4.2}/README.md +0 -0
  8. {quality_loop-0.4.1 → quality_loop-0.4.2}/pyproject.toml +0 -0
  9. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/OPERATOR.md +0 -0
  10. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/__main__.py +0 -0
  11. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/adapter.py +0 -0
  12. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/audit.py +0 -0
  13. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/awake.py +0 -0
  14. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/cli.py +0 -0
  15. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/compare.py +0 -0
  16. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/config.py +0 -0
  17. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/drive/__init__.py +0 -0
  18. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/drive/_mac.py +0 -0
  19. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/drive/_windows.py +0 -0
  20. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/drive/keys.py +0 -0
  21. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/drive/terminal.py +0 -0
  22. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/guest/boot/__init__.py +0 -0
  23. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/guest/boot/launch.py +0 -0
  24. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/guest/boot/screen_agent.py +0 -0
  25. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/guest/screens.py +0 -0
  26. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/install.py +0 -0
  27. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/mcp.py +0 -0
  28. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/operator.py +0 -0
  29. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/pages.py +0 -0
  30. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/processes.py +0 -0
  31. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/progress.py +0 -0
  32. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/report.py +0 -0
  33. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/room.py +0 -0
  34. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/runner.py +0 -0
  35. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/scenario.py +0 -0
  36. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/screens/__init__.py +0 -0
  37. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/screens/_mac.py +0 -0
  38. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/screens/_windows.py +0 -0
  39. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/session.py +0 -0
  40. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/skill/SKILL.md +0 -0
  41. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/skill/references/filing.md +0 -0
  42. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/skill/references/setup.md +0 -0
  43. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/standin.py +0 -0
  44. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/transcript.py +0 -0
  45. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop.egg-info/SOURCES.txt +0 -0
  46. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop.egg-info/dependency_links.txt +0 -0
  47. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop.egg-info/entry_points.txt +0 -0
  48. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop.egg-info/requires.txt +0 -0
  49. {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop.egg-info/top_level.txt +0 -0
  50. {quality_loop-0.4.1 → quality_loop-0.4.2}/setup.cfg +0 -0
  51. {quality_loop-0.4.1 → quality_loop-0.4.2}/tests/test_awake.py +0 -0
  52. {quality_loop-0.4.1 → quality_loop-0.4.2}/tests/test_core.py +0 -0
  53. {quality_loop-0.4.1 → quality_loop-0.4.2}/tests/test_drive.py +0 -0
  54. {quality_loop-0.4.1 → quality_loop-0.4.2}/tests/test_loop.py +0 -0
  55. {quality_loop-0.4.1 → quality_loop-0.4.2}/tests/test_mcp.py +0 -0
  56. {quality_loop-0.4.1 → quality_loop-0.4.2}/tests/test_operator.py +0 -0
  57. {quality_loop-0.4.1 → quality_loop-0.4.2}/tests/test_scenario_report.py +0 -0
  58. {quality_loop-0.4.1 → quality_loop-0.4.2}/tests/test_screens.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quality-loop
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Measure what a product teaches - its skill, tools or manual - with isolated agent runs of real tasks, reported by an operator agent
5
5
  Author: craftware
6
6
  License-Expression: MIT
@@ -12,4 +12,4 @@ room is handed, putting the user's machine back - is code here, not an
12
12
  agent's discretion.
13
13
  """
14
14
 
15
- __version__ = "0.4.1"
15
+ __version__ = "0.4.2"
@@ -483,8 +483,14 @@ class TartMachine:
483
483
  class VMwareMachine:
484
484
  """A VMware Fusion VM reverted to a snapshot before the run.
485
485
 
486
- A snapshot taken while the VM was running comes back running, signed in
487
- and with SSH up, after `vmrun start`. The SMB share that was mounted on
486
+ Take the snapshot with the VM shut down, so that every run boots it. A
487
+ snapshot taken while it ran comes back running, signed in and with SSH
488
+ up, in seconds - and with whatever was half done at that moment, every
489
+ time: Keyhac's image came back with Notepad's hand-off to its packaged
490
+ app broken, so `notepad.exe` started the inbox one, which crashed within
491
+ seconds on every run, while the same VM booted was fine. On Windows, the
492
+ run waits for the automatic sign-in, which the image must have. The SMB
493
+ share that was mounted on
488
494
  the host does not survive the revert, so it is mounted again. The VM is
489
495
  shut down afterwards, its share unmounted first: the next run reverts it
490
496
  anyway, and a VM `vmrun` left running headless is one the Fusion app
@@ -495,8 +501,7 @@ class VMwareMachine:
495
501
  Learnt on Fusion 26 with a vTPM-encrypted Windows 11 VM: `vmrun` cannot
496
502
  linked-clone it ("Cannot read the virtual machine configuration file"),
497
503
  nor snapshot it while it runs ("Authentication for encrypted virtual
498
- machine failed") - take a running-state snapshot in the Fusion app, after
499
- `vmrun suspend` (a headless VM does not show there).
504
+ machine failed"). Shut down, `vmrun -vp <password> snapshot` takes it.
500
505
  """
501
506
 
502
507
  VMRUN = "/Applications/VMware Fusion.app/Contents/Library/vmrun"
@@ -533,9 +538,36 @@ class VMwareMachine:
533
538
  f"to {self.snapshot!r}")
534
539
  self._mount()
535
540
  self._wait_for_the_clock()
541
+ if self.guest.windows:
542
+ self._wait_for_the_desktop()
536
543
  print(f"vm: {self.guest.alias} reverted to {self.snapshot!r}")
537
544
  return self
538
545
 
546
+ def _wait_for_the_desktop(self, settle: float = 15.0) -> None:
547
+ """Wait until the guest's user is signed in, and has been for
548
+ ``settle`` seconds.
549
+
550
+ A booted VM answers SSH before the automatic sign-in has made the
551
+ desktop session `launch` starts programs in, and that session's
552
+ first seconds are its startup programs; the run starts after both.
553
+ A running-state snapshot is past them at once.
554
+ """
555
+ deadline = time.monotonic() + self.patience
556
+ while time.monotonic() < deadline:
557
+ said = self.guest.ssh(
558
+ "$e = Get-Process explorer -ErrorAction SilentlyContinue | "
559
+ "Where-Object { $_.SessionId -gt 0 } | Select-Object -First 1; "
560
+ "if ($e) { [int]((Get-Date) - $e.StartTime).TotalSeconds } else { -1 }")
561
+ try:
562
+ up = int(said.stdout.strip())
563
+ except ValueError:
564
+ up = -1
565
+ if up >= settle:
566
+ return
567
+ time.sleep(settle - up if up >= 0 else 3.0)
568
+ raise GuestError(f"{self.guest.alias}: no signed-in desktop after "
569
+ f"{self.patience:.0f}s - is automatic sign-in set up?")
570
+
539
571
  def _mount(self) -> None:
540
572
  url = self.guest.settings.get("smb")
541
573
  if not url:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quality-loop
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Measure what a product teaches - its skill, tools or manual - with isolated agent runs of real tasks, reported by an operator agent
5
5
  Author: craftware
6
6
  License-Expression: MIT
@@ -208,6 +208,30 @@ def test_a_terminal_script_is_staged_and_run_with_the_guest_python(share, monkey
208
208
  assert (stage / "scripts" / "deploy.py").exists()
209
209
 
210
210
 
211
+ def test_a_booted_windows_guest_is_waited_for_until_its_desktop_has_settled(share, monkeypatch):
212
+ """A powered-off snapshot boots: SSH answers before the automatic sign-in
213
+ has made the desktop, and then its startup programs run."""
214
+ guest = _guest(share, windows=True, vmx="/v.vmx", snapshot="s")
215
+ answers = iter(["-1", "-1", "4", "16"])
216
+ ssh = FakeSSH(lambda c: (next(answers) + "\n", 0))
217
+ monkeypatch.setattr(guest, "ssh", ssh)
218
+ slept = []
219
+ monkeypatch.setattr(guests.time, "sleep", slept.append)
220
+ guests.VMwareMachine(guest)._wait_for_the_desktop(settle=15)
221
+ assert len(ssh.commands) == 4 and "explorer" in ssh.commands[0]
222
+ assert slept == [3.0, 3.0, 11]
223
+
224
+
225
+ def test_no_desktop_is_an_error_that_names_automatic_sign_in(share, monkeypatch):
226
+ guest = _guest(share, windows=True, vmx="/v.vmx", snapshot="s")
227
+ monkeypatch.setattr(guest, "ssh", FakeSSH(lambda c: ("-1\n", 0)))
228
+ clock = iter(range(0, 1000, 50))
229
+ monkeypatch.setattr(guests.time, "monotonic", lambda: next(clock))
230
+ monkeypatch.setattr(guests.time, "sleep", lambda s: None)
231
+ with pytest.raises(GuestError, match="automatic sign-in"):
232
+ guests.VMwareMachine(guest, patience=120)._wait_for_the_desktop()
233
+
234
+
211
235
  def test_machine_follows_the_settings(share):
212
236
  assert isinstance(_guest(share, tart="base").machine(), guests.TartMachine)
213
237
  assert isinstance(_guest(share, vmx="/v.vmx", snapshot="s").machine(), guests.VMwareMachine)
File without changes
File without changes
File without changes