tuieval 0.2.0.dev81__tar.gz → 0.2.0.dev86__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 (78) hide show
  1. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/CHANGELOG.md +2 -0
  2. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/PKG-INFO +1 -1
  3. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/_version.py +2 -2
  4. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/tui.py +57 -5
  5. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/tests/test_tuieval.py +76 -2
  6. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/.gitignore +0 -0
  7. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/LICENSE +0 -0
  8. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/README.md +0 -0
  9. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/RELEASING.md +0 -0
  10. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/images/brand/README.md +0 -0
  11. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/images/brand/favicon.ico +0 -0
  12. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/images/brand/readme-header.png +0 -0
  13. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/images/brand/social-preview.png +0 -0
  14. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/images/brand/tuieval-icon-1024.png +0 -0
  15. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/images/brand/tuieval-icon-128.png +0 -0
  16. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/images/brand/tuieval-icon-16.png +0 -0
  17. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/images/brand/tuieval-icon-16.svg +0 -0
  18. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/images/brand/tuieval-icon-256.png +0 -0
  19. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/images/brand/tuieval-icon-32.png +0 -0
  20. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/images/brand/tuieval-icon-48.png +0 -0
  21. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/images/brand/tuieval-icon-512.png +0 -0
  22. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/images/brand/tuieval-icon-64.png +0 -0
  23. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/images/brand/tuieval-icon-animated.svg +0 -0
  24. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/images/brand/tuieval-icon.svg +0 -0
  25. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/images/tui-answer-reasoning.png +0 -0
  26. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/images/tui-run-reasoning.png +0 -0
  27. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/images/tui-setup.png +0 -0
  28. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/models.md +0 -0
  29. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/docs/writing-packs.md +0 -0
  30. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/pyproject.toml +0 -0
  31. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/__init__.py +0 -0
  32. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/__main__.py +0 -0
  33. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/builtin.toml +0 -0
  34. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/cli.py +0 -0
  35. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/client.py +0 -0
  36. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/compare.py +0 -0
  37. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/doctor.py +0 -0
  38. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/engine.py +0 -0
  39. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/export.py +0 -0
  40. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/graders/__init__.py +0 -0
  41. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/graders/answer.py +0 -0
  42. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/graders/code.py +0 -0
  43. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/graders/rag.py +0 -0
  44. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/graders/reply.py +0 -0
  45. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/graders/tool_call.py +0 -0
  46. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/logs.py +0 -0
  47. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/machines.py +0 -0
  48. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/onboard.py +0 -0
  49. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/packs.py +0 -0
  50. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/profiles.py +0 -0
  51. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/pta.py +0 -0
  52. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/remove.py +0 -0
  53. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/run_evals.py +0 -0
  54. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/scaffold.py +0 -0
  55. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/selftest.py +0 -0
  56. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/templates/models.toml +0 -0
  57. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/templates/packs/answer/pack.toml +0 -0
  58. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/templates/packs/answer/system.txt +0 -0
  59. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
  60. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/templates/packs/code/pack.toml +0 -0
  61. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/templates/packs/code/system.txt +0 -0
  62. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/templates/packs/code/tests.yaml +0 -0
  63. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/templates/packs/rag/pack.toml +0 -0
  64. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/templates/packs/rag/system.txt +0 -0
  65. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
  66. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/templates/packs/reply/pack.toml +0 -0
  67. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/templates/packs/reply/system.txt +0 -0
  68. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
  69. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
  70. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
  71. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
  72. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
  73. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/tune.py +0 -0
  74. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/verdict.py +0 -0
  75. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/watch_proxy.py +0 -0
  76. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/workspace.py +0 -0
  77. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/src/tuieval/yamlout.py +0 -0
  78. {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev86}/tests/mock_server.py +0 -0
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Start while a run is going skips models and packs already running or queued (it says which); picking only those adds nothing. Retest and picked questions still go through.
6
+ - Fix: something queued just as a run ended (while its server was stopping) now starts instead of waiting forever.
5
7
  - Hosted models (OpenRouter and other APIs) run beside local ones instead of waiting for a slot: up to `parallel_hosted` at a time (default 4, `--parallel-hosted N`). Local models can be added to a hosted-only run.
6
8
  - Early stop: `tuieval run --stop-early`, **Stop a pack once it fails** in Setup, or `stop_early = true` in a pack.toml ends a pack once its FAIL is certain (a critical failure, or an accuracy or truncation bar out of reach), asking first the questions models fail most. Verdicts also say FAIL when a partial run can no longer pass.
7
9
  - `tuieval compare --reference <model>`: every other model paired against one on the questions both answered, per pack and overall: the difference with a 95% CI, better/tie/worse, critical failures and tokens vs the reference. `--packs` limits any compare to some packs.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tuieval
3
- Version: 0.2.0.dev81
3
+ Version: 0.2.0.dev86
4
4
  Summary: Evaluate local and frontier LLMs on your own questions: accuracy, speed, tokens and PASS/FAIL verdicts, in the terminal.
5
5
  Project-URL: Homepage, https://github.com/ashe-wb/tuieval
6
6
  Project-URL: Issues, https://github.com/ashe-wb/tuieval/issues
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.2.0.dev81'
22
- __version_tuple__ = version_tuple = (0, 2, 0, 'dev81')
21
+ __version__ = version = '0.2.0.dev86'
22
+ __version_tuple__ = version_tuple = (0, 2, 0, 'dev86')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1550,6 +1550,13 @@ def run_title(tier, models, packs):
1550
1550
  f"{', '.join(packs) if len(packs) <= 2 else f'{len(packs)} packs'}")
1551
1551
 
1552
1552
 
1553
+ def short_keys(keys, n=4):
1554
+ """A few "model/pack" pairs as "model · pack", the rest counted."""
1555
+ keys = sorted(keys)
1556
+ shown = ", ".join(k.replace("/", " · ", 1) for k in keys[:n])
1557
+ return rich_escape(shown + (f" and {len(keys) - n} more" if len(keys) > n else ""))
1558
+
1559
+
1553
1560
  class QueuedRun:
1554
1561
  """A run waiting its turn. It's planned again when it starts, so it picks up what the runs before
1555
1562
  it finished; it never sets aside answers that weren't confirmed when it was queued. retest: the
@@ -1557,13 +1564,29 @@ class QueuedRun:
1557
1564
  kind = "Run"
1558
1565
 
1559
1566
  def __init__(self, sel, retest, leave, accepted, eng):
1560
- self.sel, self.retest, self.leave, self.accepted = sel, retest, leave, accepted
1567
+ self.sel, self.retest, self.leave, self.accepted = sel, retest, set(leave), accepted
1561
1568
  self.queued = time.time()
1569
+ self.replan(eng)
1570
+
1571
+ def replan(self, eng):
1562
1572
  self.jobs, self.problem = self.plan(eng)
1563
1573
  jobs = self.jobs or []
1564
- self.title = run_title(sel["tier"], {j.label for j in jobs}, {j.pack.label for j in jobs})
1574
+ self.title = run_title(self.sel["tier"], {j.label for j in jobs}, {j.pack.label for j in jobs})
1565
1575
  self.est = eng.estimate_seconds(jobs)[0]
1566
1576
 
1577
+ def keys(self):
1578
+ """The "model/pack" pairs it covers."""
1579
+ return {f"{m}/{p}" for m in self.sel["models"] for p in self.sel["packs"]} - self.leave
1580
+
1581
+ def deliberate(self):
1582
+ """Retest or picked questions: asked again on purpose, never treated as a duplicate."""
1583
+ return bool(self.sel.get("force") or self.sel.get("tests"))
1584
+
1585
+ def drop(self, keys, eng):
1586
+ """Leave these pairs out (they're already running or queued)."""
1587
+ self.leave |= set(keys)
1588
+ self.replan(eng)
1589
+
1567
1590
  def tests(self, eng):
1568
1591
  """The picked tests that are still in their packs (a pack edited while queued drops the rest)."""
1569
1592
  out = {}
@@ -3236,8 +3259,32 @@ class EvalsApp(App):
3236
3259
  what = s.live_text() if s else "The last run is still stopping its server"
3237
3260
  return what + (f" (+{len(self.queue)} queued)" if self.queue else "")
3238
3261
 
3262
+ def taken(self, tier):
3263
+ """The "model/pack" pairs of this tier already in the run going (not finished) or in the queue."""
3264
+ out = set()
3265
+ run = self.active_session
3266
+ if isinstance(run, RunScreen) and run.running and run.jobs and run.jobs[0].tier == tier:
3267
+ out |= {j.key for j in run.jobs if j.status in ("waiting", "loading", "running")}
3268
+ for q in self.queue:
3269
+ if q.kind == "Run" and q.sel["tier"] == tier:
3270
+ out |= q.keys()
3271
+ return out
3272
+
3239
3273
  # -- the queue: Start or t while something runs adds to it; each item starts when the one before ends
3240
3274
  def start_or_queue(self, item):
3275
+ skipped = set()
3276
+ if self.is_busy() and item.kind == "Run" and not item.deliberate():
3277
+ mine = item.keys()
3278
+ skipped = mine & self.taken(item.sel["tier"])
3279
+ if skipped == mine:
3280
+ self.notify(f"Already running or queued, so nothing was added: {short_keys(skipped)}.", timeout=10)
3281
+ return
3282
+ if skipped:
3283
+ item.drop(skipped, self.engine)
3284
+ if not item.jobs:
3285
+ self.notify(f"Nothing new to run: {item.problem}. Already running or queued: "
3286
+ f"{short_keys(skipped)}.", timeout=10)
3287
+ return
3241
3288
  if not self.is_busy():
3242
3289
  screen, problem = item.make(self.engine)
3243
3290
  if problem:
@@ -3256,11 +3303,11 @@ class EvalsApp(App):
3256
3303
  self.add_to_run(run, item)
3257
3304
  elif choice == "queue":
3258
3305
  self.enqueue(item)
3259
- what = f"{', '.join(item.sel['models'][:4])}{' …' if len(item.sel['models']) > 4 else ''} · " \
3260
- f"{', '.join(item.sel['packs'][:4])}{' …' if len(item.sel['packs']) > 4 else ''}"
3306
+ what = short_keys(item.keys(), 6)
3307
+ skip = (f"\n\nLeft out, already running or queued: {short_keys(skipped)}." if skipped else "")
3261
3308
  self.push_screen(ChoiceScreen(
3262
3309
  f"A run is going: {run.live_text()}.\n\nAdd {what} to it? Its models run after the ones "
3263
- "already in it.\nOr queue it as a separate run that starts when this one ends.",
3310
+ f"already in it.\nOr queue it as a separate run that starts when this one ends.{skip}",
3264
3311
  [("add", "Add to this run", "success"), ("queue", f"Queue it as #{len(self.queue) + 1}", "primary"),
3265
3312
  ("keep", "Cancel", "default")]), picked)
3266
3313
  return
@@ -3270,6 +3317,9 @@ class EvalsApp(App):
3270
3317
  jobs, problem = item.plan(self.engine)
3271
3318
  added, why = self.engine.add_jobs(jobs) if jobs else ([], problem)
3272
3319
  if not added:
3320
+ if why and why.startswith("everything picked is already in the run"):
3321
+ self.notify(f"Not added: {why}.", timeout=10) # queueing it again would only duplicate it
3322
+ return
3273
3323
  self.notify(f"Not added: {why}. Queued it instead.", severity="warning", timeout=10)
3274
3324
  self.enqueue(item)
3275
3325
  return
@@ -3281,6 +3331,8 @@ class EvalsApp(App):
3281
3331
  def enqueue(self, item):
3282
3332
  ahead = self.busy_text()
3283
3333
  self.queue.append(item)
3334
+ if not self.active_session: # the last run already ended (its server may still be stopping):
3335
+ self.set_timer(0.3, self.start_next) # nothing else would start the queue
3284
3336
  self.notify(f"Queued #{len(self.queue)}: {item.title}. It starts by itself after: {ahead}. "
3285
3337
  "w shows the queue.", timeout=10)
3286
3338
  self.refresh_live()
@@ -1007,6 +1007,10 @@ class AddToRun(unittest.TestCase):
1007
1007
  import asyncio, json, time
1008
1008
  from tuieval.tui import EvalsApp, RunScreen, SetupScreen, ChoiceScreen
1009
1009
  from textual.widgets import SelectionList
1010
+ async def setup(app, pilot): # a dialog closing can take a moment on a slow machine
1011
+ deadline = time.time() + 30
1012
+ while not isinstance(app.screen, SetupScreen) and time.time() < deadline:
1013
+ await pilot.pause(0.1)
1010
1014
  async def go():
1011
1015
  app = EvalsApp({})
1012
1016
  async with app.run_test(size=(160, 50)) as pilot:
@@ -1033,7 +1037,7 @@ class AddToRun(unittest.TestCase):
1033
1037
  await pilot.pause()
1034
1038
  asked = isinstance(app.screen, ChoiceScreen)
1035
1039
  app.screen.dismiss("add")
1036
- await pilot.pause()
1040
+ await setup(app, pilot)
1037
1041
  app.screen.query_one("#suites", SelectionList).deselect("apps")
1038
1042
  app.screen.query_one("#suites", SelectionList).select("other")
1039
1043
  app.screen.selected_models = {"a"}
@@ -1041,7 +1045,7 @@ class AddToRun(unittest.TestCase):
1041
1045
  app.screen.action_start()
1042
1046
  await pilot.pause()
1043
1047
  app.screen.dismiss("queue")
1044
- await pilot.pause()
1048
+ await setup(app, pilot)
1045
1049
  app.screen.query_one("#tier-smoke").value = True # another tier can't join: queued
1046
1050
  await pilot.pause()
1047
1051
  app.screen.action_start()
@@ -1074,6 +1078,76 @@ class AddToRun(unittest.TestCase):
1074
1078
  self.assertEqual(out["second"], [["a/other", "done"]]) # queued: its own run after
1075
1079
 
1076
1080
 
1081
+ def test_tui_never_queues_the_same_thing_twice(self):
1082
+ """Start while a run is going never adds or queues a model and pack already running or queued."""
1083
+ code = textwrap.dedent("""
1084
+ import asyncio, json, time
1085
+ from tuieval.tui import EvalsApp, RunScreen, SetupScreen, ChoiceScreen
1086
+ from textual.widgets import SelectionList
1087
+ async def pick(app, pilot, models, packs=("apps",)):
1088
+ deadline = time.time() + 30
1089
+ while not isinstance(app.screen, SetupScreen) and time.time() < deadline:
1090
+ await pilot.pause(0.1)
1091
+ s = app.screen
1092
+ for p in ("apps", "other"):
1093
+ (s.query_one("#suites", SelectionList).select if p in packs
1094
+ else s.query_one("#suites", SelectionList).deselect)(p)
1095
+ s.selected_models = set(models)
1096
+ s.refresh_models()
1097
+ await pilot.pause()
1098
+ s.action_start()
1099
+ await pilot.pause()
1100
+ return app.screen
1101
+ async def go():
1102
+ app = EvalsApp({})
1103
+ async with app.run_test(size=(160, 50)) as pilot:
1104
+ await pilot.pause()
1105
+ app.screen.query_one("#tier-certify").value = True
1106
+ await pick(app, pilot, ["a"])
1107
+ deadline = time.time() + 180
1108
+ while time.time() < deadline:
1109
+ await pilot.pause(0.1)
1110
+ if isinstance(app.screen, RunScreen) and app.screen.counts()[0]:
1111
+ break
1112
+ run = app.screen
1113
+ await pilot.press("n")
1114
+ out = {}
1115
+ s = await pick(app, pilot, ["a"]) # the same again: nothing happens
1116
+ out["same"] = [isinstance(s, ChoiceScreen), len(app.queue)]
1117
+ s = await pick(app, pilot, ["a", "b"]) # partly new: only b is offered
1118
+ out["partly"] = [isinstance(s, ChoiceScreen), "Left out" in str(getattr(s, "question", ""))]
1119
+ s.dismiss("queue")
1120
+ await pilot.pause()
1121
+ out["queued"] = [sorted(q.keys()) for q in app.queue]
1122
+ s = await pick(app, pilot, ["b"]) # already queued: nothing happens
1123
+ out["again"] = [isinstance(s, ChoiceScreen), len(app.queue)]
1124
+ s = await pick(app, pilot, ["a"], ("other",)) # new: offered, queued or started
1125
+ out["new"] = (isinstance(s, ChoiceScreen) or any("a/other" in q.keys() for q in app.queue)
1126
+ or any(j.key == "a/other" for x in app.sessions for j in x.jobs))
1127
+ if isinstance(s, ChoiceScreen):
1128
+ s.dismiss("queue")
1129
+ await pilot.pause()
1130
+ deadline = time.time() + 300 # the queued runs start one after another: wait for all three
1131
+ while time.time() < deadline:
1132
+ await pilot.pause(0.2)
1133
+ if len(app.sessions) == 3 and not app.queue and not any(x.running for x in app.sessions):
1134
+ break
1135
+ out["sessions"] = sorted((j.key, j.status) for x in app.sessions for j in x.jobs)
1136
+ print(json.dumps(out))
1137
+ asyncio.run(go())
1138
+ """)
1139
+ p = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=600,
1140
+ env=dict(os.environ, TUIEVAL_HOME=self.ws, TUIEVAL_DETECT_PORTS=""))
1141
+ out = json.loads(p.stdout.strip().splitlines()[-1]) if p.stdout.strip() else p.stderr
1142
+ self.assertIsInstance(out, dict, out)
1143
+ self.assertEqual(out["same"], [False, 0])
1144
+ self.assertEqual(out["partly"], [True, True])
1145
+ self.assertEqual(out["queued"], [["b/apps"]])
1146
+ self.assertEqual(out["again"], [False, 1])
1147
+ self.assertTrue(out["new"])
1148
+ self.assertEqual(out["sessions"], [["a/apps", "done"], ["a/other", "done"], ["b/apps", "done"]])
1149
+
1150
+
1077
1151
  class PTAIndex(unittest.TestCase):
1078
1152
  """The PTA index: parsimony from tokens and speed from time, each relative to the best on a log
1079
1153
  scale, and accuracy, all over the questions every compared model answered."""
File without changes
File without changes
File without changes