@wendellresearch-ai/benchwarmers 0.2.0 → 0.2.2

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 (103) hide show
  1. package/AGENTS.md +152 -147
  2. package/BENCHMARKS.md +262 -262
  3. package/GATEWAY.md +415 -272
  4. package/NOTICE +28 -28
  5. package/PRIVACY.md +46 -46
  6. package/README.md +408 -314
  7. package/SECURITY.md +83 -83
  8. package/TERMS.md +22 -22
  9. package/USAGE.md +186 -163
  10. package/VERSION +1 -1
  11. package/bin/benchwarmers.js +16 -74
  12. package/cli.py +674 -0
  13. package/examples/config/gateway.json +5 -5
  14. package/examples/config/model-roles.json +11 -11
  15. package/gateway.py +939 -755
  16. package/launcher.py +120 -0
  17. package/mcp_server.py +266 -266
  18. package/metrics/alerts.py +116 -116
  19. package/metrics/aliases.py +41 -41
  20. package/metrics/bench_collections.py +141 -141
  21. package/metrics/catalog.py +304 -295
  22. package/metrics/client.py +804 -394
  23. package/metrics/cliproxy.py +191 -191
  24. package/metrics/connect.py +691 -691
  25. package/metrics/cost.py +171 -152
  26. package/metrics/decide.py +227 -227
  27. package/metrics/discover.py +318 -318
  28. package/metrics/endpoints.py +436 -436
  29. package/metrics/formats.py +1021 -0
  30. package/metrics/guard.py +647 -592
  31. package/metrics/learn.py +234 -221
  32. package/metrics/ledger.py +148 -0
  33. package/metrics/litellm_hub.py +206 -206
  34. package/metrics/model_roles.py +94 -84
  35. package/metrics/offload.py +166 -166
  36. package/metrics/owui.py +205 -205
  37. package/metrics/pacing.py +248 -0
  38. package/metrics/paths.py +60 -60
  39. package/metrics/quota.py +383 -0
  40. package/metrics/route.py +581 -579
  41. package/metrics/scheduler.py +275 -275
  42. package/metrics/sources.py +167 -167
  43. package/metrics/wrong.py +144 -144
  44. package/package.json +69 -65
  45. package/perf/README.md +89 -89
  46. package/perf/run.py +284 -261
  47. package/scripts/auto_retire.py +367 -367
  48. package/scripts/check.py +113 -106
  49. package/scripts/check_deprecations.py +378 -378
  50. package/scripts/check_themes.py +103 -103
  51. package/scripts/init.py +124 -124
  52. package/scripts/list_served.py +78 -78
  53. package/scripts/notice.py +78 -78
  54. package/scripts/onboard.py +410 -331
  55. package/scripts/provider_tags.py +129 -129
  56. package/scripts/reprefix.py +136 -136
  57. package/scripts/sync_config.py +105 -105
  58. package/scripts/test_auth.py +255 -255
  59. package/scripts/test_cli.py +189 -0
  60. package/scripts/test_client.py +277 -233
  61. package/scripts/test_connect.py +222 -222
  62. package/scripts/test_first_run.py +180 -180
  63. package/scripts/test_formats.py +549 -0
  64. package/scripts/test_hubs.py +328 -328
  65. package/scripts/test_launcher.py +67 -0
  66. package/scripts/test_llm_log.py +85 -81
  67. package/scripts/test_mcp.py +178 -178
  68. package/scripts/test_merged.py +215 -214
  69. package/scripts/test_no_owui.py +291 -291
  70. package/scripts/test_owui.py +90 -90
  71. package/scripts/test_quota.py +334 -0
  72. package/scripts/test_serve_edit.py +453 -438
  73. package/scripts/test_server_json.py +79 -0
  74. package/scripts/test_verdict.py +102 -98
  75. package/scripts/test_vision.py +231 -228
  76. package/serve.py +3972 -3764
  77. package/suite/mine.py +154 -154
  78. package/suite/pace.py +145 -145
  79. package/suite/run.py +1046 -1042
  80. package/suite/seed.py +421 -421
  81. package/suite/tasks/claim-alias-unbound/fixtures/onboard.py +286 -286
  82. package/suite/tasks/claim-error-text-string/fixtures/decide.py +187 -187
  83. package/suite/tasks/claim-list-body/fixtures/gateway.py +235 -235
  84. package/suite/tasks/claim-member-nominated-twice/fixtures/gateway.py +284 -284
  85. package/suite/tasks/claim-nominee-uses-slot/fixtures/gateway.py +284 -284
  86. package/suite/tasks/claim-silent-exit/fixtures/gateway.py +235 -235
  87. package/suite/tasks/claim-sse-hang/fixtures/gateway.py +235 -235
  88. package/suite/tasks/claim-unknown-nominee-crash/fixtures/gateway.py +284 -284
  89. package/suite/tasks/rebench-matchms-809-bug/task.json +23 -23
  90. package/suite/tasks/rebench-matchms-809-control/task.json +27 -27
  91. package/suite/tasks/rebench-more-itertools-1028-bug/task.json +23 -23
  92. package/suite/tasks/rebench-more-itertools-1028-control/task.json +27 -27
  93. package/suite/tasks/rebench-narwhals-2864-bug/task.json +23 -23
  94. package/suite/tasks/rebench-narwhals-2864-control/task.json +27 -27
  95. package/suite/tasks/rebench-quantecon-py-769-bug/task.json +23 -23
  96. package/suite/tasks/rebench-quantecon-py-769-control/task.json +27 -27
  97. package/suite/tasks/rebench-scrapy-6867-bug/task.json +23 -23
  98. package/suite/tasks/rebench-scrapy-6867-control/task.json +27 -27
  99. package/ui/bench.css +430 -425
  100. package/ui/bench.js +1757 -1576
  101. package/ui/themes/dark.css +35 -35
  102. package/ui/themes/hardwood.css +52 -52
  103. package/ui/themes/rainforest.css +32 -32
@@ -1,180 +1,180 @@
1
- """A stranger's first run, on an empty data folder. No real files.
2
-
3
- python scripts/test_first_run.py
4
-
5
- RELEASE-PLAN Phases 1 and 4. The dashboard renders with no endpoint and leads
6
- into Add, through the real handler; a job that needs an endpoint still
7
- refuses, saying how to add one; the readers of seed files fall back to empty
8
- when a file is missing; the shipped roles and shapes merge under the
9
- install's own; init copies examples/, points a checkout at the folder (a
10
- temp pointer here) and adds a first endpoint by save_endpoint's rules
11
- against a fake server. BENCHWARMERS_HOME and LLM_USER_PATH name temp folders
12
- before anything imports metrics/paths.py, so no install's data and not the
13
- real llm key store are touched.
14
- """
15
-
16
- from __future__ import annotations
17
-
18
- import json
19
- import os
20
- import pathlib
21
- import shutil
22
- import subprocess
23
- import sys
24
- import tempfile
25
- import threading
26
- import urllib.error
27
- import urllib.request
28
- from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
29
-
30
- ROOT = pathlib.Path(__file__).resolve().parent.parent
31
- TMP = pathlib.Path(tempfile.mkdtemp())
32
- DATA, LLM = TMP / "data", TMP / "llm"
33
- DATA.mkdir()
34
- os.environ.update(BENCHWARMERS_HOME=str(DATA), LLM_USER_PATH=str(LLM))
35
- sys.path[:0] = [str(ROOT), str(ROOT / "scripts")]
36
- import serve # noqa: E402
37
- import init # noqa: E402
38
- import auto_retire # noqa: E402
39
- import onboard # noqa: E402
40
- import model_roles # noqa: E402
41
- import paths # noqa: E402
42
- import route # noqa: E402
43
- import run as suite # noqa: E402
44
-
45
-
46
- class Provider(BaseHTTPRequestHandler):
47
- def do_GET(self): # noqa: N802
48
- body = json.dumps({"data": [{"id": "m1"}, {"id": "m2"}]} if self.path == "/v1/models" else {}).encode()
49
- self.send_response(200 if self.path == "/v1/models" else 404)
50
- self.send_header("Content-Type", "application/json")
51
- self.send_header("Content-Length", str(len(body)))
52
- self.end_headers()
53
- self.wfile.write(body)
54
-
55
- def log_message(self, *_a):
56
- pass
57
-
58
-
59
- def serve_on(handler) -> ThreadingHTTPServer:
60
- server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
61
- threading.Thread(target=server.serve_forever, daemon=True).start()
62
- return server
63
-
64
-
65
- def main() -> int:
66
- assert serve.DATA == DATA and serve.endpoints.LLM_DIR == LLM, "not on the temp folders"
67
- # The npm package (bin/benchwarmers.js) carries the app at the app's version.
68
- pkg = json.loads((ROOT / "package.json").read_text(encoding="utf-8"))
69
- assert pkg["version"] == (ROOT / "VERSION").read_text(encoding="utf-8").strip(), "package.json and VERSION differ"
70
- provider, dash = serve_on(Provider), serve_on(serve.Handler)
71
- serve.PORT = dash.server_port
72
- home = f"http://127.0.0.1:{dash.server_port}"
73
-
74
- def get(path: str):
75
- with urllib.request.urlopen(home + path, timeout=10) as r:
76
- body = r.read().decode("utf-8")
77
- return json.loads(body) if path.startswith("/api/") else body
78
-
79
- def post(path: str, data: dict) -> tuple[int, dict]:
80
- req = urllib.request.Request(home + path, json.dumps(data).encode(), method="POST",
81
- headers={"Content-Type": "application/json"})
82
- try:
83
- with urllib.request.urlopen(req, timeout=10) as r:
84
- return r.status, json.loads(r.read())
85
- except urllib.error.HTTPError as exc:
86
- return exc.code, json.loads(exc.read())
87
-
88
- try:
89
- # 0. An endpoints file that cannot be read is not a fresh install: the
90
- # page says which file and why (load() refuses, so no writer saves over it).
91
- broken = DATA / "config" / "endpoints.json"
92
- broken.parent.mkdir()
93
- broken.write_text("{", encoding="utf-8")
94
- assert "cannot be read" in get("/"), "a broken endpoints file read as none"
95
- broken.unlink()
96
-
97
- # 1. No endpoint: the page renders and leads into Add; the APIs answer.
98
- page = get("/")
99
- assert "id='first-run' data-step='endpoint'" in page and "id='e-add'" in page, "no first-run step"
100
- assert get("/api/endpoints")["endpoints"] == []
101
- assert get("/api/connect")["providers"], "no providers to sign in with"
102
- cand = get("/api/candidates")
103
- assert not cand["ok"] and "add one first" in cand["errors"][0], cand
104
- assert get("/api/collections")["ok"] and get("/api/connections")["ok"]
105
- status, res = post("/api/onboard", {"served": "m1"})
106
- assert status == 400 and "no endpoints configured" in res["errors"][0], res # a reply, not a dropped request
107
- # ...while a job that needs an endpoint refuses, saying how to add one.
108
- empty = TMP / "job"
109
- empty.mkdir()
110
- job = subprocess.run([sys.executable, "scripts/check_deprecations.py", "--dry-run"], cwd=ROOT,
111
- capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=120,
112
- env={**os.environ, "BENCHWARMERS_HOME": str(empty)})
113
- assert job.returncode and "no endpoints configured" in job.stderr, job.stderr[-500:]
114
- assert not any(empty.iterdir()), "a refused job wrote something"
115
-
116
- # 2. Every seed file missing: the readers say "nothing yet".
117
- assert suite.registered_models() == [] and suite._registered_names() == {}
118
- assert suite.available_models() == []
119
- assert auto_retire.evidence() == ([], {}, {})
120
- assert auto_retire.run({}, set(), {}, dry_run=True) == ([], set())
121
- assert onboard.existing_aliases() == {}
122
- assert onboard.alias_for("v/m1", {"m1": "other/m1"}) == "m1-alt" # a collision, no provider tags
123
- assert model_roles.load() == {"roles": model_roles.ROLES, "shapes": model_roles.SHAPES,
124
- "models": {}, "ad_hoc": []}
125
- assert route.roles_for_shape("review") == ["general", "code"] and route.role_of("x") == "general"
126
-
127
- # 3. A file from before the split (all four keys): its own wording wins,
128
- # name by name, and what it does not define still comes from the product.
129
- roles_file = DATA / "config" / "model-roles.json"
130
- roles_file.parent.mkdir(parents=True, exist_ok=True)
131
- roles_file.write_text(json.dumps({"roles": {"code": "mine"}, "shapes": {"review": ["general"],
132
- "mine": ["light"]}, "models": {"a": "code"}, "ad_hoc": ["b"]}), encoding="utf-8")
133
- merged = model_roles.load()
134
- assert merged["roles"]["code"] == "mine" and merged["roles"]["light"] == model_roles.ROLES["light"]
135
- assert merged["shapes"]["review"] == ["general"] and merged["shapes"]["mine"] == ["light"]
136
- assert merged["shapes"]["quick"] == model_roles.SHAPES["quick"]
137
- assert (merged["models"], merged["ad_hoc"]) == ({"a": "code"}, ["b"])
138
- assert route.roles_for_shape("mine") == ["light"] and "mine" in serve.collections_view()["shapes"]
139
- roles_file.unlink()
140
-
141
- # 4. init: examples copied, the layout made, the checkout pointed, a first endpoint added.
142
- init.POINTER = TMP / "pointer"
143
- os.environ.pop("BENCHWARMERS_HOME")
144
- ok, said = init.run(DATA, ("fake", f"http://127.0.0.1:{provider.server_port}/v1"), "sk-test")
145
- assert ok, said
146
- assert json.loads(roles_file.read_text(encoding="utf-8"))["models"] == {}
147
- assert (DATA / "config" / "gateway.json").exists() and all((DATA / d).is_dir() for d in paths.LAYOUT)
148
- assert init.POINTER.read_text(encoding="utf-8").strip() == str(DATA.resolve())
149
- assert list(serve.endpoints.load()) == ["fake"] and serve.endpoints.default() == "fake"
150
- assert json.loads((LLM / "keys.json").read_text(encoding="utf-8")) == {"fake": "sk-test"}
151
- assert "data-step='onboard'" in get("/"), "the next step is not onboarding"
152
- serve.endpoints.YAML.write_text(serve.endpoints.yaml_block("m1", "m1", "fake"), encoding="utf-8")
153
- assert "data-step='collection'" in serve.first_run(), "a model on the bench, and no collection"
154
- (DATA / "config" / "collections.json").write_text('{"c": {"members": ["m1"]}}', encoding="utf-8")
155
- assert serve.first_run() == "", "set up, yet still a first-run step"
156
- # Again, on a folder in use: nothing overwritten, the pointer kept.
157
- (DATA / "config" / "gateway.json").write_text('{"max_calls": 3}', encoding="utf-8")
158
- os.environ.pop("BENCHWARMERS_HOME")
159
- ok, said = init.run(DATA)
160
- assert ok and "0 starter file(s)" in said[0] and "already points" in said[1], said
161
- assert json.loads((DATA / "config" / "gateway.json").read_text(encoding="utf-8")) == {"max_calls": 3}
162
- # A checkout already pointing elsewhere, or BENCHWARMERS_HOME set: left alone, and said so.
163
- init.POINTER.write_text("elsewhere\n", encoding="utf-8")
164
- os.environ.pop("BENCHWARMERS_HOME")
165
- ok, said = init.run(TMP / "other")
166
- assert ok and "left so" in said[1] and init.POINTER.read_text(encoding="utf-8") == "elsewhere\n", said
167
- assert (TMP / "other" / "config" / "model-roles.json").exists()
168
- os.environ["BENCHWARMERS_HOME"] = str(TMP / "mine")
169
- ok, said = init.run(TMP / "third")
170
- assert ok and "which wins" in said[1] and init.POINTER.read_text(encoding="utf-8") == "elsewhere\n", said
171
- finally:
172
- provider.shutdown()
173
- dash.shutdown()
174
- shutil.rmtree(TMP, ignore_errors=True)
175
- print("ok")
176
- return 0
177
-
178
-
179
- if __name__ == "__main__":
180
- sys.exit(main())
1
+ """A stranger's first run, on an empty data folder. No real files.
2
+
3
+ python scripts/test_first_run.py
4
+
5
+ RELEASE-PLAN Phases 1 and 4. The dashboard renders with no endpoint and leads
6
+ into Add, through the real handler; a job that needs an endpoint still
7
+ refuses, saying how to add one; the readers of seed files fall back to empty
8
+ when a file is missing; the shipped roles and shapes merge under the
9
+ install's own; init copies examples/, points a checkout at the folder (a
10
+ temp pointer here) and adds a first endpoint by save_endpoint's rules
11
+ against a fake server. BENCHWARMERS_HOME and LLM_USER_PATH name temp folders
12
+ before anything imports metrics/paths.py, so no install's data and not the
13
+ real llm key store are touched.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import pathlib
21
+ import shutil
22
+ import subprocess
23
+ import sys
24
+ import tempfile
25
+ import threading
26
+ import urllib.error
27
+ import urllib.request
28
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
29
+
30
+ ROOT = pathlib.Path(__file__).resolve().parent.parent
31
+ TMP = pathlib.Path(tempfile.mkdtemp())
32
+ DATA, LLM = TMP / "data", TMP / "llm"
33
+ DATA.mkdir()
34
+ os.environ.update(BENCHWARMERS_HOME=str(DATA), LLM_USER_PATH=str(LLM))
35
+ sys.path[:0] = [str(ROOT), str(ROOT / "scripts")]
36
+ import serve # noqa: E402
37
+ import init # noqa: E402
38
+ import auto_retire # noqa: E402
39
+ import onboard # noqa: E402
40
+ import model_roles # noqa: E402
41
+ import paths # noqa: E402
42
+ import route # noqa: E402
43
+ import run as suite # noqa: E402
44
+
45
+
46
+ class Provider(BaseHTTPRequestHandler):
47
+ def do_GET(self): # noqa: N802
48
+ body = json.dumps({"data": [{"id": "m1"}, {"id": "m2"}]} if self.path == "/v1/models" else {}).encode()
49
+ self.send_response(200 if self.path == "/v1/models" else 404)
50
+ self.send_header("Content-Type", "application/json")
51
+ self.send_header("Content-Length", str(len(body)))
52
+ self.end_headers()
53
+ self.wfile.write(body)
54
+
55
+ def log_message(self, *_a):
56
+ pass
57
+
58
+
59
+ def serve_on(handler) -> ThreadingHTTPServer:
60
+ server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
61
+ threading.Thread(target=server.serve_forever, daemon=True).start()
62
+ return server
63
+
64
+
65
+ def main() -> int:
66
+ assert serve.DATA == DATA and serve.endpoints.LLM_DIR == LLM, "not on the temp folders"
67
+ # The npm package (bin/benchwarmers.js) carries the app at the app's version.
68
+ pkg = json.loads((ROOT / "package.json").read_text(encoding="utf-8"))
69
+ assert pkg["version"] == (ROOT / "VERSION").read_text(encoding="utf-8").strip(), "package.json and VERSION differ"
70
+ provider, dash = serve_on(Provider), serve_on(serve.Handler)
71
+ serve.PORT = dash.server_port
72
+ home = f"http://127.0.0.1:{dash.server_port}"
73
+
74
+ def get(path: str):
75
+ with urllib.request.urlopen(home + path, timeout=10) as r:
76
+ body = r.read().decode("utf-8")
77
+ return json.loads(body) if path.startswith("/api/") else body
78
+
79
+ def post(path: str, data: dict) -> tuple[int, dict]:
80
+ req = urllib.request.Request(home + path, json.dumps(data).encode(), method="POST",
81
+ headers={"Content-Type": "application/json"})
82
+ try:
83
+ with urllib.request.urlopen(req, timeout=10) as r:
84
+ return r.status, json.loads(r.read())
85
+ except urllib.error.HTTPError as exc:
86
+ return exc.code, json.loads(exc.read())
87
+
88
+ try:
89
+ # 0. An endpoints file that cannot be read is not a fresh install: the
90
+ # page says which file and why (load() refuses, so no writer saves over it).
91
+ broken = DATA / "config" / "endpoints.json"
92
+ broken.parent.mkdir()
93
+ broken.write_text("{", encoding="utf-8")
94
+ assert "cannot be read" in get("/"), "a broken endpoints file read as none"
95
+ broken.unlink()
96
+
97
+ # 1. No endpoint: the page renders and leads into Add; the APIs answer.
98
+ page = get("/")
99
+ assert "id='first-run' data-step='endpoint'" in page and "id='e-add'" in page, "no first-run step"
100
+ assert get("/api/endpoints")["endpoints"] == []
101
+ assert get("/api/connect")["providers"], "no providers to sign in with"
102
+ cand = get("/api/candidates")
103
+ assert not cand["ok"] and "add one first" in cand["errors"][0], cand
104
+ assert get("/api/collections")["ok"] and get("/api/connections")["ok"]
105
+ status, res = post("/api/onboard", {"served": "m1"})
106
+ assert status == 400 and "no endpoints configured" in res["errors"][0], res # a reply, not a dropped request
107
+ # ...while a job that needs an endpoint refuses, saying how to add one.
108
+ empty = TMP / "job"
109
+ empty.mkdir()
110
+ job = subprocess.run([sys.executable, "scripts/check_deprecations.py", "--dry-run"], cwd=ROOT,
111
+ capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=120,
112
+ env={**os.environ, "BENCHWARMERS_HOME": str(empty)})
113
+ assert job.returncode and "no endpoints configured" in job.stderr, job.stderr[-500:]
114
+ assert not any(empty.iterdir()), "a refused job wrote something"
115
+
116
+ # 2. Every seed file missing: the readers say "nothing yet".
117
+ assert suite.registered_models() == [] and suite._registered_names() == {}
118
+ assert suite.available_models() == []
119
+ assert auto_retire.evidence() == ([], {}, {})
120
+ assert auto_retire.run({}, set(), {}, dry_run=True) == ([], set())
121
+ assert onboard.existing_aliases() == {}
122
+ assert onboard.alias_for("v/m1", {"m1": "other/m1"}) == "m1-alt" # a collision, no provider tags
123
+ assert model_roles.load() == {"roles": model_roles.ROLES, "shapes": model_roles.SHAPES,
124
+ "models": {}, "ad_hoc": []}
125
+ assert route.roles_for_shape("review") == ["general", "code"] and route.role_of("x") == "general"
126
+
127
+ # 3. A file from before the split (all four keys): its own wording wins,
128
+ # name by name, and what it does not define still comes from the product.
129
+ roles_file = DATA / "config" / "model-roles.json"
130
+ roles_file.parent.mkdir(parents=True, exist_ok=True)
131
+ roles_file.write_text(json.dumps({"roles": {"code": "mine"}, "shapes": {"review": ["general"],
132
+ "mine": ["light"]}, "models": {"a": "code"}, "ad_hoc": ["b"]}), encoding="utf-8")
133
+ merged = model_roles.load()
134
+ assert merged["roles"]["code"] == "mine" and merged["roles"]["light"] == model_roles.ROLES["light"]
135
+ assert merged["shapes"]["review"] == ["general"] and merged["shapes"]["mine"] == ["light"]
136
+ assert merged["shapes"]["quick"] == model_roles.SHAPES["quick"]
137
+ assert (merged["models"], merged["ad_hoc"]) == ({"a": "code"}, ["b"])
138
+ assert route.roles_for_shape("mine") == ["light"] and "mine" in serve.collections_view()["shapes"]
139
+ roles_file.unlink()
140
+
141
+ # 4. init: examples copied, the layout made, the checkout pointed, a first endpoint added.
142
+ init.POINTER = TMP / "pointer"
143
+ os.environ.pop("BENCHWARMERS_HOME")
144
+ ok, said = init.run(DATA, ("fake", f"http://127.0.0.1:{provider.server_port}/v1"), "sk-test")
145
+ assert ok, said
146
+ assert json.loads(roles_file.read_text(encoding="utf-8"))["models"] == {}
147
+ assert (DATA / "config" / "gateway.json").exists() and all((DATA / d).is_dir() for d in paths.LAYOUT)
148
+ assert init.POINTER.read_text(encoding="utf-8").strip() == str(DATA.resolve())
149
+ assert list(serve.endpoints.load()) == ["fake"] and serve.endpoints.default() == "fake"
150
+ assert json.loads((LLM / "keys.json").read_text(encoding="utf-8")) == {"fake": "sk-test"}
151
+ assert "data-step='onboard'" in get("/"), "the next step is not onboarding"
152
+ serve.endpoints.YAML.write_text(serve.endpoints.yaml_block("m1", "m1", "fake"), encoding="utf-8")
153
+ assert "data-step='collection'" in serve.first_run(), "a model on the bench, and no collection"
154
+ (DATA / "config" / "collections.json").write_text('{"c": {"members": ["m1"]}}', encoding="utf-8")
155
+ assert serve.first_run() == "", "set up, yet still a first-run step"
156
+ # Again, on a folder in use: nothing overwritten, the pointer kept.
157
+ (DATA / "config" / "gateway.json").write_text('{"max_calls": 3}', encoding="utf-8")
158
+ os.environ.pop("BENCHWARMERS_HOME")
159
+ ok, said = init.run(DATA)
160
+ assert ok and "0 starter file(s)" in said[0] and "already points" in said[1], said
161
+ assert json.loads((DATA / "config" / "gateway.json").read_text(encoding="utf-8")) == {"max_calls": 3}
162
+ # A checkout already pointing elsewhere, or BENCHWARMERS_HOME set: left alone, and said so.
163
+ init.POINTER.write_text("elsewhere\n", encoding="utf-8")
164
+ os.environ.pop("BENCHWARMERS_HOME")
165
+ ok, said = init.run(TMP / "other")
166
+ assert ok and "left so" in said[1] and init.POINTER.read_text(encoding="utf-8") == "elsewhere\n", said
167
+ assert (TMP / "other" / "config" / "model-roles.json").exists()
168
+ os.environ["BENCHWARMERS_HOME"] = str(TMP / "mine")
169
+ ok, said = init.run(TMP / "third")
170
+ assert ok and "which wins" in said[1] and init.POINTER.read_text(encoding="utf-8") == "elsewhere\n", said
171
+ finally:
172
+ provider.shutdown()
173
+ dash.shutdown()
174
+ shutil.rmtree(TMP, ignore_errors=True)
175
+ print("ok")
176
+ return 0
177
+
178
+
179
+ if __name__ == "__main__":
180
+ sys.exit(main())