bazis-cli 2.5.0__tar.gz → 2.6.1__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.
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/PKG-INFO +5 -4
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/README.md +3 -2
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis/contrib/cli/_version.py +3 -3
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis_cli.egg-info/PKG-INFO +5 -4
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis_cli.egg-info/SOURCES.txt +2 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis_cli.egg-info/requires.txt +1 -1
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis_cli.egg-info/scm_file_list.json +2 -0
- bazis_cli-2.6.1/bazis_cli.egg-info/scm_version.json +8 -0
- bazis_cli-2.6.1/docs/releases/2.6.0.md +15 -0
- bazis_cli-2.6.1/docs/releases/2.6.1.md +9 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/evals/grade.py +127 -26
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/pyproject.toml +1 -1
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/tests/test_cli.py +34 -5
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/tests/test_evals.py +161 -5
- bazis_cli-2.5.0/bazis_cli.egg-info/scm_version.json +0 -8
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/.dockerignore +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/.github/workflows/build-publish.yml +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/.github/workflows/release.yml +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/.github/workflows/tests.yml +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/.github/workflows/update-licenses.yml +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/.gitignore +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/CLAUDE.md +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis/contrib/cli/AGENTS.md +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis/contrib/cli/__init__.py +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis/contrib/cli/agent.py +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis/contrib/cli/bazis_manifest.toml +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis/contrib/cli/main.py +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis_cli.egg-info/dependency_links.txt +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis_cli.egg-info/entry_points.txt +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis_cli.egg-info/top_level.txt +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/docs/releases/2.4.0.md +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/docs/releases/2.4.1.md +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/docs/releases/2.5.0.md +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/evals/__init__.py +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/evals/run.py +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/evals/tasks.toml +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/setup.cfg +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/tests/__init__.py +0 -0
- {bazis_cli-2.5.0 → bazis_cli-2.6.1}/tests/conftest.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bazis-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.1
|
|
4
4
|
Summary: The bazis command: creates, extends and audits Bazis products, backend and frontend, with Claude (Claude Agent SDK).
|
|
5
5
|
Author-email: Ilya Kharyn <ilya.tt07@gmail.com>
|
|
6
6
|
Maintainer-email: Ilya Kharyn <ilya.tt07@gmail.com>
|
|
@@ -19,7 +19,7 @@ Classifier: Framework :: FastAPI
|
|
|
19
19
|
Requires-Python: >=3.12
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
Requires-Dist: bazis>=2.16.0
|
|
22
|
-
Requires-Dist: bazis-mcp>=2.
|
|
22
|
+
Requires-Dist: bazis-mcp>=2.6.0
|
|
23
23
|
Requires-Dist: claude-agent-sdk>=0.2.163
|
|
24
24
|
Provides-Extra: test
|
|
25
25
|
Requires-Dist: pytest>=8; extra == "test"
|
|
@@ -82,8 +82,9 @@ bazis change # the changes typed one after another; an empty line ends
|
|
|
82
82
|
[bazis-front](https://github.com/ecofuture-tech/bazis-front): the specs of the product
|
|
83
83
|
(`spec/`: roles, entities, workflows, access, scenarios, screens, design), the backend
|
|
84
84
|
that satisfies them, the contract exported from it (`contract/`), a React + TypeScript
|
|
85
|
-
frontend (`frontend/`) with the screens
|
|
86
|
-
|
|
85
|
+
frontend (`frontend/`) with the screens generated from their specs (`bazis_front
|
|
86
|
+
screens`), the end-to-end tests of the scenarios run against the backend and the
|
|
87
|
+
screenshots of the screens looked at by the agent. It needs Node.js
|
|
87
88
|
22.12 or newer with npm, and PostgreSQL for the contract and the end-to-end tests;
|
|
88
89
|
`--no-frontend` builds only the backend.
|
|
89
90
|
- The product is in the language of its description and in English, the language of the
|
|
@@ -54,8 +54,9 @@ bazis change # the changes typed one after another; an empty line ends
|
|
|
54
54
|
[bazis-front](https://github.com/ecofuture-tech/bazis-front): the specs of the product
|
|
55
55
|
(`spec/`: roles, entities, workflows, access, scenarios, screens, design), the backend
|
|
56
56
|
that satisfies them, the contract exported from it (`contract/`), a React + TypeScript
|
|
57
|
-
frontend (`frontend/`) with the screens
|
|
58
|
-
|
|
57
|
+
frontend (`frontend/`) with the screens generated from their specs (`bazis_front
|
|
58
|
+
screens`), the end-to-end tests of the scenarios run against the backend and the
|
|
59
|
+
screenshots of the screens looked at by the agent. It needs Node.js
|
|
59
60
|
22.12 or newer with npm, and PostgreSQL for the contract and the end-to-end tests;
|
|
60
61
|
`--no-frontend` builds only the backend.
|
|
61
62
|
- The product is in the language of its description and in English, the language of the
|
|
@@ -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 = '2.
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.6.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 6, 1)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g27dcdfabb'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bazis-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.1
|
|
4
4
|
Summary: The bazis command: creates, extends and audits Bazis products, backend and frontend, with Claude (Claude Agent SDK).
|
|
5
5
|
Author-email: Ilya Kharyn <ilya.tt07@gmail.com>
|
|
6
6
|
Maintainer-email: Ilya Kharyn <ilya.tt07@gmail.com>
|
|
@@ -19,7 +19,7 @@ Classifier: Framework :: FastAPI
|
|
|
19
19
|
Requires-Python: >=3.12
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
Requires-Dist: bazis>=2.16.0
|
|
22
|
-
Requires-Dist: bazis-mcp>=2.
|
|
22
|
+
Requires-Dist: bazis-mcp>=2.6.0
|
|
23
23
|
Requires-Dist: claude-agent-sdk>=0.2.163
|
|
24
24
|
Provides-Extra: test
|
|
25
25
|
Requires-Dist: pytest>=8; extra == "test"
|
|
@@ -82,8 +82,9 @@ bazis change # the changes typed one after another; an empty line ends
|
|
|
82
82
|
[bazis-front](https://github.com/ecofuture-tech/bazis-front): the specs of the product
|
|
83
83
|
(`spec/`: roles, entities, workflows, access, scenarios, screens, design), the backend
|
|
84
84
|
that satisfies them, the contract exported from it (`contract/`), a React + TypeScript
|
|
85
|
-
frontend (`frontend/`) with the screens
|
|
86
|
-
|
|
85
|
+
frontend (`frontend/`) with the screens generated from their specs (`bazis_front
|
|
86
|
+
screens`), the end-to-end tests of the scenarios run against the backend and the
|
|
87
|
+
screenshots of the screens looked at by the agent. It needs Node.js
|
|
87
88
|
22.12 or newer with npm, and PostgreSQL for the contract and the end-to-end tests;
|
|
88
89
|
`--no-frontend` builds only the backend.
|
|
89
90
|
- The product is in the language of its description and in English, the language of the
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# bazis-cli 2.6.0
|
|
2
|
+
|
|
3
|
+
## Changes
|
|
4
|
+
|
|
5
|
+
- bazis-cli requires bazis-mcp 2.6.0 (`bazis-mcp>=2.6.0`): the tasks and the rules of
|
|
6
|
+
bazis-front 0.3.0. A new product requires bazis-front 0.3.0; its screens are generated
|
|
7
|
+
from their specs (`bazis_front screens`), with an extension (`extend`) or a custom screen
|
|
8
|
+
(`custom`) only for what a spec cannot say, and its screenshots (`bazis_front shots`)
|
|
9
|
+
are taken and looked at, at most three rounds. A change of a product generates its
|
|
10
|
+
screens again and ends with the whole round of the final checks.
|
|
11
|
+
- The evals grade the screens of a product: `bazis_front screens --check` and `bazis_front
|
|
12
|
+
shots --check` (a comparison with the specs, the screenshots not taken by the grader)
|
|
13
|
+
with `design` and `e2e`, and no TSX of the product in `frontend/src/` but the code of its
|
|
14
|
+
custom and extended screens (the copies, the generated screens and the template are not
|
|
15
|
+
its own); the detail counts the lines of TSX written by hand.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# bazis-cli 2.6.1
|
|
2
|
+
|
|
3
|
+
## Changes
|
|
4
|
+
|
|
5
|
+
- The evals grader keeps the settings of the machine (the database and GDAL) and creates
|
|
6
|
+
the database on the server of the project.
|
|
7
|
+
- The check that the migrations apply does not pass silently: when the database server is
|
|
8
|
+
unknown or createdb is missing, the check fails.
|
|
9
|
+
- The test of the requirements to the latest releases of bazis and bazis-mcp is restored.
|
|
@@ -18,14 +18,16 @@ Python of the project (`.venv`) against PostgreSQL and Redis of `.env`. It does
|
|
|
18
18
|
on the names the agent chose: it checks that the project loads and passes its checks, has
|
|
19
19
|
the expected Bazis packages, complete migrations that apply, models and routes of its own,
|
|
20
20
|
and tests that pass. A product with a frontend (`frontend = true`) also needs specs valid
|
|
21
|
-
against a fresh contract, generated files that are not stale,
|
|
22
|
-
|
|
21
|
+
against a fresh contract, generated files that are not stale, screens generated from their
|
|
22
|
+
specs (no TSX of its own but that of its custom and extended screens), a frontend that
|
|
23
|
+
builds and passes its lint and component tests, and end-to-end tests of its scenarios that pass
|
|
23
24
|
against the backend with the test data of `manage.py e2e_data` (Node.js is needed). A task
|
|
24
25
|
with a `language` needs the product in that language and in English, with its catalog.
|
|
25
26
|
The grader runs on POSIX only (macOS, Linux): it stops the servers and the commands it
|
|
26
27
|
starts by their process group.
|
|
27
28
|
"""
|
|
28
29
|
|
|
30
|
+
import json
|
|
29
31
|
import os
|
|
30
32
|
import re
|
|
31
33
|
import secrets
|
|
@@ -49,6 +51,32 @@ BAZIS_CHECK_PREFIXES = (
|
|
|
49
51
|
'async_request.', 'bg.', 'front.',
|
|
50
52
|
)
|
|
51
53
|
TIMEOUT = 900
|
|
54
|
+
#: the settings of the machine that the agent was given and the project must keep when it is
|
|
55
|
+
#: graded: the PostgreSQL and Redis servers and the GDAL and GEOS libraries. The other `BS_*`
|
|
56
|
+
#: variables are those of the project (its `.env`), the name of the database is the grader's
|
|
57
|
+
MACHINE_SETTINGS = (
|
|
58
|
+
'BS_DATABASES__DEFAULT__HOST', 'BS_DATABASES__DEFAULT__PORT', 'BS_DATABASES__DEFAULT__USER',
|
|
59
|
+
'BS_DATABASES__DEFAULT__PASSWORD', 'BS_CACHES__DEFAULT__LOCATION',
|
|
60
|
+
'BS_GDAL_LIBRARY_PATH', 'BS_GEOS_LIBRARY_PATH',
|
|
61
|
+
)
|
|
62
|
+
#: run by `manage.py shell` of the project: prints the server of its database as JSON
|
|
63
|
+
DATABASE_SERVER = """\
|
|
64
|
+
import json
|
|
65
|
+
from django.conf import settings
|
|
66
|
+
db = settings.DATABASES['default']
|
|
67
|
+
print(json.dumps({key: db.get(key.upper()) or None for key in ('host', 'port', 'user', 'password')}))
|
|
68
|
+
"""
|
|
69
|
+
#: the TSX of `frontend/src/` that a product does not write: the generated screens and the
|
|
70
|
+
#: template (its entry, its app and its login), besides the assets that bazis-front copied
|
|
71
|
+
#: (the files of `assets` in its lock, among them the shadcn components of `components/ui/`)
|
|
72
|
+
NOT_WRITTEN = ('bazis/', 'screens/generated/', 'main.tsx', 'app/', 'screens/login/')
|
|
73
|
+
#: prints `custom` and `extend` of the specs of the screens by their ids, as JSON (run with the
|
|
74
|
+
#: Python of the project, which has PyYAML with bazis-front)
|
|
75
|
+
SCREEN_SPECS = """\
|
|
76
|
+
import json, pathlib, yaml
|
|
77
|
+
specs = [yaml.safe_load(path.read_text(encoding='utf-8')) for path in pathlib.Path('spec/screens').glob('*.yaml')]
|
|
78
|
+
print(json.dumps({it['id']: {key: it[key] for key in ('custom', 'extend') if key in it} for it in specs}))
|
|
79
|
+
"""
|
|
52
80
|
|
|
53
81
|
|
|
54
82
|
@dataclass
|
|
@@ -119,13 +147,27 @@ def local_apps(project: Path) -> set[str]:
|
|
|
119
147
|
|
|
120
148
|
def project_env(database: str) -> dict:
|
|
121
149
|
"""
|
|
122
|
-
The environment of the project: its own .env and project.env
|
|
150
|
+
The environment of the project: its own .env and project.env and the settings of the
|
|
151
|
+
machine (`MACHINE_SETTINGS`), a database of its own.
|
|
123
152
|
"""
|
|
124
|
-
env = {
|
|
153
|
+
env = {
|
|
154
|
+
k: v for k, v in os.environ.items()
|
|
155
|
+
if k in MACHINE_SETTINGS or not k.startswith(('BS_', 'DJANGO_'))
|
|
156
|
+
}
|
|
125
157
|
env['BS_DATABASES__DEFAULT__NAME'] = database
|
|
126
158
|
return env
|
|
127
159
|
|
|
128
160
|
|
|
161
|
+
def postgres_command(program: str, server: dict, *args: str) -> list[str]:
|
|
162
|
+
"""
|
|
163
|
+
A PostgreSQL client command (`createdb`, `dropdb`) for the server of the project.
|
|
164
|
+
"""
|
|
165
|
+
command = [program, '-h', server['host'] or 'localhost', '-U', server['user'] or 'postgres']
|
|
166
|
+
if server['port']:
|
|
167
|
+
command += ['-p', str(server['port'])]
|
|
168
|
+
return [*command, *args]
|
|
169
|
+
|
|
170
|
+
|
|
129
171
|
def package_warnings(messages: list[dict]) -> list[str]:
|
|
130
172
|
"""
|
|
131
173
|
The ids of the warnings of the Bazis packages among the messages of `bazis_doctor`; the
|
|
@@ -183,26 +225,7 @@ def grade(project: Path, task: dict) -> list[Check]:
|
|
|
183
225
|
|
|
184
226
|
if task.get('frontend'):
|
|
185
227
|
checks += frontend_checks(project, env, task.get('min_scenarios', 1))
|
|
186
|
-
|
|
187
|
-
created = subprocess.run(
|
|
188
|
-
['createdb', '-h', 'localhost', '-U', 'postgres', database],
|
|
189
|
-
env={**env, 'PGPASSWORD': 'postgres'}, capture_output=True, text=True,
|
|
190
|
-
)
|
|
191
|
-
try:
|
|
192
|
-
migrate = run(project, ['manage.py', 'migrate', '--noinput'], env)
|
|
193
|
-
migrated = created.returncode == 0 and migrate.returncode == 0
|
|
194
|
-
checks.append(Check(
|
|
195
|
-
'migrations apply', migrated, created.stderr.strip() or tail(migrate, 6),
|
|
196
|
-
))
|
|
197
|
-
if task.get('frontend'):
|
|
198
|
-
checks += frontend_database_checks(project, env, migrated)
|
|
199
|
-
finally:
|
|
200
|
-
subprocess.run(
|
|
201
|
-
['dropdb', '-h', 'localhost', '-U', 'postgres', '--if-exists', '--force', database],
|
|
202
|
-
env={**env, 'PGPASSWORD': 'postgres'}, capture_output=True,
|
|
203
|
-
)
|
|
204
|
-
elif task.get('frontend'):
|
|
205
|
-
checks += frontend_database_checks(project, env, False, 'no PostgreSQL (createdb)')
|
|
228
|
+
checks += database_checks(project, env, database, bool(task.get('frontend')))
|
|
206
229
|
|
|
207
230
|
collected = run(project, ['-m', 'pytest', '--collect-only', '-q', '-p', 'no:cacheprovider'], env)
|
|
208
231
|
count = re.search(r'(\d+) tests? collected', collected.stdout)
|
|
@@ -216,6 +239,44 @@ def grade(project: Path, task: dict) -> list[Check]:
|
|
|
216
239
|
return checks
|
|
217
240
|
|
|
218
241
|
|
|
242
|
+
def database_checks(project: Path, env: dict, database: str, frontend: bool) -> list[Check]:
|
|
243
|
+
"""
|
|
244
|
+
The checks that need the database of the grading: the migrations apply to a database
|
|
245
|
+
created on the server of the project (dropped afterwards), then the checks of the
|
|
246
|
+
frontend on it. Without `createdb` or the server of the project they fail with the
|
|
247
|
+
reason instead of dropping out of the result.
|
|
248
|
+
"""
|
|
249
|
+
server = None
|
|
250
|
+
if shutil.which('createdb'):
|
|
251
|
+
probe = run(project, ['manage.py', 'shell', '-c', DATABASE_SERVER], env)
|
|
252
|
+
server = mcp_project.parse_json(probe.stdout)
|
|
253
|
+
reason = 'server of the database: ' + (tail(probe, 6) or 'no output')
|
|
254
|
+
else:
|
|
255
|
+
reason = 'no PostgreSQL (createdb)'
|
|
256
|
+
if not server:
|
|
257
|
+
checks = [Check('migrations apply', False, reason)]
|
|
258
|
+
if frontend:
|
|
259
|
+
checks += frontend_database_checks(project, env, False, reason)
|
|
260
|
+
return checks
|
|
261
|
+
client_env = {**env, 'PGPASSWORD': server['password'] or ''}
|
|
262
|
+
created = subprocess.run(
|
|
263
|
+
postgres_command('createdb', server, database),
|
|
264
|
+
env=client_env, capture_output=True, text=True,
|
|
265
|
+
)
|
|
266
|
+
try:
|
|
267
|
+
migrate = run(project, ['manage.py', 'migrate', '--noinput'], env)
|
|
268
|
+
migrated = created.returncode == 0 and migrate.returncode == 0
|
|
269
|
+
checks = [Check('migrations apply', migrated, created.stderr.strip() or tail(migrate, 6))]
|
|
270
|
+
if frontend:
|
|
271
|
+
checks += frontend_database_checks(project, env, migrated)
|
|
272
|
+
return checks
|
|
273
|
+
finally:
|
|
274
|
+
subprocess.run(
|
|
275
|
+
postgres_command('dropdb', server, '--if-exists', '--force', database),
|
|
276
|
+
env=client_env, capture_output=True,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
219
280
|
def language_check(project: Path, info: dict, language: str, env: dict) -> Check:
|
|
220
281
|
"""
|
|
221
282
|
The product is in the language of its description and in English (`LANGUAGES`), the
|
|
@@ -278,12 +339,14 @@ def frontend_checks(project: Path, env: dict, min_scenarios: int) -> list[Check]
|
|
|
278
339
|
stale = [
|
|
279
340
|
done for done in (
|
|
280
341
|
run(project, ['manage.py', 'bazis_front', command, '--check'], env)
|
|
281
|
-
for command in ('design', 'e2e')
|
|
342
|
+
for command in ('design', 'screens', 'e2e', 'shots')
|
|
282
343
|
) if done.returncode
|
|
283
344
|
]
|
|
284
345
|
checks.append(Check(
|
|
285
|
-
'theme
|
|
346
|
+
'theme, screens, end-to-end tests and screenshots generated', not stale,
|
|
347
|
+
'\n'.join(tail(d, 4) for d in stale),
|
|
286
348
|
))
|
|
349
|
+
checks.append(written_screens(project, env))
|
|
287
350
|
|
|
288
351
|
build = npm(frontend, ['run', 'build'], env)
|
|
289
352
|
checks.append(Check('frontend builds', build.returncode == 0, tail(build, 8)))
|
|
@@ -298,6 +361,44 @@ def frontend_checks(project: Path, env: dict, min_scenarios: int) -> list[Check]
|
|
|
298
361
|
return checks
|
|
299
362
|
|
|
300
363
|
|
|
364
|
+
def written_screens(project: Path, env: dict) -> Check:
|
|
365
|
+
"""
|
|
366
|
+
The screens are generated from their specs: the TSX that the product writes in
|
|
367
|
+
`frontend/src/` (not NOT_WRITTEN) is only the code of a custom screen (`custom`: its
|
|
368
|
+
directory `screens/<id>/`, or the module it names) or of an extended one (`extend`: its
|
|
369
|
+
directory). The detail counts the lines of that TSX, a measure of the hand-written code.
|
|
370
|
+
"""
|
|
371
|
+
title = 'no TSX of its own but that of the custom and extended screens'
|
|
372
|
+
specs = run(project, ['-c', SCREEN_SPECS], env)
|
|
373
|
+
screens = mcp_project.parse_json(specs.stdout)
|
|
374
|
+
if screens is None:
|
|
375
|
+
return Check(title, False, 'the specs of the screens: ' + tail(specs, 4))
|
|
376
|
+
allowed = []
|
|
377
|
+
for id_, spec in screens.items():
|
|
378
|
+
custom = spec.get('custom')
|
|
379
|
+
if custom is True or spec.get('extend'):
|
|
380
|
+
allowed.append(f'screens/{id_}/')
|
|
381
|
+
elif custom:
|
|
382
|
+
module = custom['module'].removeprefix('@/')
|
|
383
|
+
allowed += [f'{module}.tsx', f'{module}/']
|
|
384
|
+
source = project / 'frontend' / 'src'
|
|
385
|
+
lock = json.loads((project / 'frontend' / 'bazis-front.lock.json').read_text(encoding='utf-8'))
|
|
386
|
+
copied = {
|
|
387
|
+
file.removeprefix('src/') for asset in lock['assets'].values() for file in asset.get('files', ())
|
|
388
|
+
}
|
|
389
|
+
written = {
|
|
390
|
+
name: path for path in sorted(source.rglob('*.tsx'))
|
|
391
|
+
if not (name := path.relative_to(source).as_posix()).startswith(NOT_WRITTEN)
|
|
392
|
+
and name not in copied
|
|
393
|
+
}
|
|
394
|
+
lines = sum(len(path.read_text(encoding='utf-8').splitlines()) for path in written.values())
|
|
395
|
+
other = [name for name in written if not name.startswith(tuple(allowed))]
|
|
396
|
+
detail = f'lines of TSX written by hand: {lines}'
|
|
397
|
+
if other:
|
|
398
|
+
detail += '; not of a custom or extended screen: ' + ', '.join(other)
|
|
399
|
+
return Check(title, not other, detail)
|
|
400
|
+
|
|
401
|
+
|
|
301
402
|
def project_package(project: Path) -> str:
|
|
302
403
|
"""
|
|
303
404
|
The project package, from DJANGO_SETTINGS_MODULE of manage.py.
|
|
@@ -37,7 +37,7 @@ dependencies = [
|
|
|
37
37
|
# the settings of a project without .venv (--no-venv), the languages of Django and the
|
|
38
38
|
# modules a project package must not hide are those of the Python of the CLI
|
|
39
39
|
"bazis>=2.16.0",
|
|
40
|
-
"bazis-mcp>=2.
|
|
40
|
+
"bazis-mcp>=2.6.0",
|
|
41
41
|
"claude-agent-sdk>=0.2.163",
|
|
42
42
|
]
|
|
43
43
|
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
import io
|
|
16
16
|
import json
|
|
17
17
|
import sys
|
|
18
|
+
import tomllib
|
|
19
|
+
from importlib import metadata
|
|
18
20
|
from pathlib import Path
|
|
19
21
|
from types import SimpleNamespace
|
|
20
22
|
|
|
@@ -337,8 +339,11 @@ def test_add_to_a_product_with_a_frontend(tmp_path, fake_agent, capsys):
|
|
|
337
339
|
assert 'package_guide("bazis-statusy")' in backend
|
|
338
340
|
# a project made before the ports gets them
|
|
339
341
|
assert prompts.ports_text(scaffold.project_ports(tmp_path)) in frontend
|
|
340
|
-
|
|
341
|
-
|
|
342
|
+
# the screens generated again, then the whole round of the final checks (bazis-mcp 2.6)
|
|
343
|
+
frontend = ' '.join(frontend.split())
|
|
344
|
+
order = ['spec/', 'bazis_front contract', 'front_check', 'bazis_front screens',
|
|
345
|
+
'bazis_front e2e', 'e2e_data', 'the whole round of the final checks of the rules',
|
|
346
|
+
'npm run build', 'bazis_front screens --check']
|
|
342
347
|
positions = [frontend.index(text) for text in order]
|
|
343
348
|
assert positions == sorted(positions)
|
|
344
349
|
assert FRONTEND_RULES in options.system_prompt['append']
|
|
@@ -359,7 +364,7 @@ def test_add_the_frontend(tmp_path, fake_agent):
|
|
|
359
364
|
assert '"Building the frontend of a product"' in prompt
|
|
360
365
|
assert 'the final checks of the rules' in prompt
|
|
361
366
|
assert 'the specs describe the backend and what the project needs it for' in prompt
|
|
362
|
-
assert 'requirement `
|
|
367
|
+
assert f'requirement `{scaffold.FRONTEND_REQUIREMENT}`' in prompt
|
|
363
368
|
assert FRONTEND_RULES in options.system_prompt['append']
|
|
364
369
|
|
|
365
370
|
|
|
@@ -372,7 +377,7 @@ def test_add_the_frontend_from_a_build(tmp_path, fake_agent, monkeypatch):
|
|
|
372
377
|
|
|
373
378
|
prompt, _ = fake_agent[0]
|
|
374
379
|
assert 'requirement `/wheels/bazis_front-0.1.0.whl`' in prompt
|
|
375
|
-
assert
|
|
380
|
+
assert scaffold.FRONTEND_REQUIREMENT not in prompt
|
|
376
381
|
|
|
377
382
|
|
|
378
383
|
def test_add_without_node(tmp_path, fake_agent, monkeypatch, capsys):
|
|
@@ -505,7 +510,11 @@ def test_change(tmp_path, fake_agent, sessions, capsys):
|
|
|
505
510
|
assert main.main(['change', 'Add the authors', *project, '--budget', '2']) == 0
|
|
506
511
|
prompt, options = fake_agent[-1]
|
|
507
512
|
assert options.resume == 's' and options.max_budget_usd == 2
|
|
508
|
-
|
|
513
|
+
# the request, the final checks once, and the headings of the summary (bazis-mcp 2.6)
|
|
514
|
+
assert prompt == (
|
|
515
|
+
'Add the authors\n\nAfter the change, run the final checks of the rules once.\n\n'
|
|
516
|
+
f'{prompts.SUMMARY}\n'
|
|
517
|
+
)
|
|
509
518
|
|
|
510
519
|
assert main.main(['change', 'Add the authors', *project, '--new-session']) == 0
|
|
511
520
|
assert fake_agent[-1][1].resume is None
|
|
@@ -608,3 +617,23 @@ def test_change_without_a_terminal(tmp_path, fake_agent, monkeypatch, capsys):
|
|
|
608
617
|
assert 'bazis change "..."' in capsys.readouterr().err
|
|
609
618
|
assert fake_agent == []
|
|
610
619
|
|
|
620
|
+
|
|
621
|
+
def test_requirements_are_the_latest_releases():
|
|
622
|
+
"""
|
|
623
|
+
The CLI requires at least the releases that bazis-mcp describes: its catalog of the
|
|
624
|
+
released packages gives bazis, and the installed bazis-mcp is the release of that catalog.
|
|
625
|
+
"""
|
|
626
|
+
from bazis.contrib.mcp import catalog
|
|
627
|
+
|
|
628
|
+
def version(text):
|
|
629
|
+
return tuple(int(part) for part in text.split('.')[:3])
|
|
630
|
+
|
|
631
|
+
installed = metadata.version('bazis-mcp')
|
|
632
|
+
if 'dev' in installed:
|
|
633
|
+
pytest.skip(f'bazis-mcp {installed} is not a release')
|
|
634
|
+
pyproject = tomllib.loads((Path(__file__).parent.parent / 'pyproject.toml').read_text())
|
|
635
|
+
released = {**{name: entry['version'] for name, entry in catalog.catalog().items()}, 'bazis-mcp': installed}
|
|
636
|
+
pins = dict(it.split('>=') for it in pyproject['project']['dependencies'] if it.startswith('bazis'))
|
|
637
|
+
assert pins.keys() == {'bazis', 'bazis-mcp'}
|
|
638
|
+
for name, minimum in pins.items():
|
|
639
|
+
assert version(minimum) >= version(released[name]), f'{name}>={minimum}, released {released[name]}'
|
|
@@ -12,10 +12,16 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
import contextlib
|
|
16
|
+
import io
|
|
15
17
|
import json
|
|
16
18
|
import os
|
|
17
19
|
import subprocess
|
|
20
|
+
import sys
|
|
18
21
|
import time
|
|
22
|
+
import types
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
19
25
|
|
|
20
26
|
from bazis.contrib.mcp import catalog, scaffold
|
|
21
27
|
|
|
@@ -96,11 +102,45 @@ def test_local_apps(tmp_path):
|
|
|
96
102
|
def test_project_env_isolates_the_database(monkeypatch):
|
|
97
103
|
monkeypatch.setenv('BS_DEBUG', 'false')
|
|
98
104
|
monkeypatch.setenv('DJANGO_SETTINGS_MODULE', 'other.settings')
|
|
105
|
+
monkeypatch.setenv('BS_DATABASES__DEFAULT__NAME', 'other')
|
|
99
106
|
env = grade.project_env('eval_x')
|
|
100
107
|
assert env['BS_DATABASES__DEFAULT__NAME'] == 'eval_x'
|
|
101
108
|
assert 'BS_DEBUG' not in env and 'DJANGO_SETTINGS_MODULE' not in env
|
|
102
109
|
|
|
103
110
|
|
|
111
|
+
def test_project_env_keeps_the_settings_of_the_machine(monkeypatch):
|
|
112
|
+
machine = {
|
|
113
|
+
'BS_DATABASES__DEFAULT__PORT': '55910', 'BS_GDAL_LIBRARY_PATH': '/opt/lib/libgdal.dylib',
|
|
114
|
+
'BS_GEOS_LIBRARY_PATH': '/opt/lib/libgeos_c.dylib', 'PGHOST': 'db', 'DYLD_LIBRARY_PATH': '/opt/lib',
|
|
115
|
+
}
|
|
116
|
+
for key, value in machine.items():
|
|
117
|
+
monkeypatch.setenv(key, value)
|
|
118
|
+
assert machine.items() <= grade.project_env('eval_x').items()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_database_server_of_the_project(monkeypatch):
|
|
122
|
+
settings = types.SimpleNamespace(DATABASES={'default': {
|
|
123
|
+
'HOST': 'db', 'PORT': '55910', 'USER': 'owner', 'PASSWORD': 'secret', 'NAME': 'x',
|
|
124
|
+
}})
|
|
125
|
+
monkeypatch.setitem(sys.modules, 'django', types.ModuleType('django'))
|
|
126
|
+
monkeypatch.setitem(sys.modules, 'django.conf', types.SimpleNamespace(settings=settings))
|
|
127
|
+
output = io.StringIO()
|
|
128
|
+
with contextlib.redirect_stdout(output):
|
|
129
|
+
exec(grade.DATABASE_SERVER, {})
|
|
130
|
+
server = json.loads(output.getvalue())
|
|
131
|
+
assert server == {'host': 'db', 'port': '55910', 'user': 'owner', 'password': 'secret'}
|
|
132
|
+
assert grade.postgres_command('createdb', server, 'eval_x') == [
|
|
133
|
+
'createdb', '-h', 'db', '-U', 'owner', '-p', '55910', 'eval_x',
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_postgres_command_defaults():
|
|
138
|
+
server = {'host': None, 'port': None, 'user': None, 'password': None}
|
|
139
|
+
assert grade.postgres_command('dropdb', server, '--force', 'x') == [
|
|
140
|
+
'dropdb', '-h', 'localhost', '-U', 'postgres', '--force', 'x',
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
|
|
104
144
|
def test_summary(tmp_path):
|
|
105
145
|
checks = [grade.Check('project loads', True), grade.Check('tests pass', False, 'boom\n1 failed')]
|
|
106
146
|
result = {'task': 'tracker', 'seconds': 120, 'cost_usd': 1.5, 'turns': 30,
|
|
@@ -111,8 +151,6 @@ def test_summary(tmp_path):
|
|
|
111
151
|
|
|
112
152
|
|
|
113
153
|
def test_running_needs_a_budget():
|
|
114
|
-
import pytest
|
|
115
|
-
|
|
116
154
|
with pytest.raises(SystemExit, match='budget'):
|
|
117
155
|
run.main_cli(['--task', 'tracker'])
|
|
118
156
|
|
|
@@ -125,7 +163,7 @@ def product(tmp_path, scenarios: int):
|
|
|
125
163
|
scaffold.write_files(tmp_path, 'desk', frontend=True)
|
|
126
164
|
generated = tmp_path / 'frontend' / 'e2e' / 'generated'
|
|
127
165
|
generated.mkdir(parents=True)
|
|
128
|
-
(tmp_path / 'frontend' / 'bazis-front.lock.json').write_text('{}')
|
|
166
|
+
(tmp_path / 'frontend' / 'bazis-front.lock.json').write_text('{"assets": {}}')
|
|
129
167
|
for index in range(scenarios):
|
|
130
168
|
(generated / f'scenario-{index}.spec.ts').write_text('')
|
|
131
169
|
(generated / 'product.ts').write_text('')
|
|
@@ -139,6 +177,8 @@ def test_frontend_checks(tmp_path, monkeypatch):
|
|
|
139
177
|
|
|
140
178
|
def fake_run(project, args, env):
|
|
141
179
|
commands.append(args)
|
|
180
|
+
if args[0] == '-c':
|
|
181
|
+
return done(0, '{}')
|
|
142
182
|
if args[2] == 'check':
|
|
143
183
|
return done(1, json.dumps({'contract': True, 'errors': 1, 'warnings': 1, 'issues': issues}))
|
|
144
184
|
return done(1 if args[2] == 'e2e' else 0)
|
|
@@ -153,10 +193,82 @@ def test_frontend_checks(tmp_path, monkeypatch):
|
|
|
153
193
|
assert checks['specs valid against the contract'].detail == 'P019 spec/product.yaml#/roles/0'
|
|
154
194
|
assert not checks['at least 3 scenarios'].passed
|
|
155
195
|
assert checks['at least 3 scenarios'].detail == 'scenario-0.spec, scenario-1.spec'
|
|
156
|
-
assert not checks['theme
|
|
196
|
+
assert not checks['theme, screens, end-to-end tests and screenshots generated'].passed
|
|
197
|
+
assert checks['no TSX of its own but that of the custom and extended screens'].passed
|
|
157
198
|
assert checks['frontend builds'].passed
|
|
158
199
|
assert not checks['frontend lint and component tests pass'].passed
|
|
159
|
-
|
|
200
|
+
for command in ('design', 'screens', 'e2e', 'shots'):
|
|
201
|
+
assert ['manage.py', 'bazis_front', command, '--check'] in commands
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_written_screens(tmp_path, monkeypatch):
|
|
205
|
+
"""
|
|
206
|
+
The TSX that a product writes is only that of its custom and extended screens: not of a
|
|
207
|
+
screen generated from its spec, nor elsewhere in `src/`; the copies, the generated
|
|
208
|
+
screens and the template are not its own. The lines written are counted.
|
|
209
|
+
"""
|
|
210
|
+
screens = {
|
|
211
|
+
'loan-list': {'custom': True},
|
|
212
|
+
'book-card': {'extend': True},
|
|
213
|
+
'reader-list': {'custom': {'module': '@/screens/readers', 'export': 'ReaderListScreen'}},
|
|
214
|
+
'book-list': {},
|
|
215
|
+
}
|
|
216
|
+
monkeypatch.setattr(grade, 'run', lambda project, args, env: done(0, json.dumps(screens)))
|
|
217
|
+
source = tmp_path / 'frontend' / 'src'
|
|
218
|
+
files = {
|
|
219
|
+
# the custom and extended screens, their own files too
|
|
220
|
+
'screens/loan-list/index.tsx': 3, 'screens/loan-list/parts.tsx': 2,
|
|
221
|
+
'screens/book-card/extend.tsx': 4, 'screens/readers.tsx': 1,
|
|
222
|
+
# not the product's
|
|
223
|
+
'screens/generated/book-list.tsx': 50, 'bazis/ui/list/index.tsx': 50,
|
|
224
|
+
'app/router.tsx': 50, 'main.tsx': 50, 'screens/login/index.tsx': 50,
|
|
225
|
+
'screens/book-list/index.tsx': 5, 'components/badge.tsx': 6,
|
|
226
|
+
'screens/loan-list-extra/index.tsx': 7, 'i18n/en.ts': 50,
|
|
227
|
+
# the screen of a custom module elsewhere has no directory of its own
|
|
228
|
+
'screens/reader-list/index.tsx': 8,
|
|
229
|
+
}
|
|
230
|
+
for name, lines in files.items():
|
|
231
|
+
(source / name).parent.mkdir(parents=True, exist_ok=True)
|
|
232
|
+
(source / name).write_text('x\n' * lines)
|
|
233
|
+
# the shadcn components that bazis-front copied are in its lock: not the product's
|
|
234
|
+
(source / 'components' / 'ui').mkdir(parents=True)
|
|
235
|
+
(source / 'components' / 'ui' / 'button.tsx').write_text('x\n' * 100)
|
|
236
|
+
(source.parent / 'bazis-front.lock.json').write_text(json.dumps({
|
|
237
|
+
# an entry of an old dev-lock has no `files`
|
|
238
|
+
'assets': {'button': {'files': {'src/components/ui/button.tsx': 'sha256:0'}}, 'old': {}},
|
|
239
|
+
}))
|
|
240
|
+
|
|
241
|
+
check = grade.written_screens(tmp_path, {})
|
|
242
|
+
|
|
243
|
+
assert not check.passed and check.name.startswith('no TSX of its own')
|
|
244
|
+
assert check.detail == (
|
|
245
|
+
'lines of TSX written by hand: 36; not of a custom or extended screen: '
|
|
246
|
+
'components/badge.tsx, screens/book-list/index.tsx, screens/loan-list-extra/index.tsx, '
|
|
247
|
+
'screens/reader-list/index.tsx'
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
for name in ('screens/book-list/index.tsx', 'components/badge.tsx', 'screens/loan-list-extra/index.tsx',
|
|
251
|
+
'screens/reader-list/index.tsx'):
|
|
252
|
+
(source / name).unlink()
|
|
253
|
+
check = grade.written_screens(tmp_path, {})
|
|
254
|
+
assert check.passed and check.detail == 'lines of TSX written by hand: 10'
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_written_screens_reads_the_specs(tmp_path):
|
|
258
|
+
"""
|
|
259
|
+
The specs of the screens are read with the Python of the project (PyYAML of bazis-front).
|
|
260
|
+
"""
|
|
261
|
+
pytest.importorskip('yaml')
|
|
262
|
+
(tmp_path / 'spec' / 'screens').mkdir(parents=True)
|
|
263
|
+
(tmp_path / 'spec' / 'screens' / 'loan-list.yaml').write_text('id: loan-list\ncustom: true\n')
|
|
264
|
+
(tmp_path / 'spec' / 'screens' / 'book-list.yaml').write_text('id: book-list\nprimitive: list\n')
|
|
265
|
+
(tmp_path / 'frontend' / 'src' / 'screens' / 'loan-list').mkdir(parents=True)
|
|
266
|
+
(tmp_path / 'frontend' / 'src' / 'screens' / 'loan-list' / 'index.tsx').write_text('x\n')
|
|
267
|
+
(tmp_path / 'frontend' / 'bazis-front.lock.json').write_text('{"assets": {}}')
|
|
268
|
+
|
|
269
|
+
check = grade.written_screens(tmp_path, {**os.environ})
|
|
270
|
+
|
|
271
|
+
assert check.passed and check.detail == 'lines of TSX written by hand: 1'
|
|
160
272
|
|
|
161
273
|
|
|
162
274
|
def test_frontend_checks_without_a_frontend(tmp_path):
|
|
@@ -173,6 +285,50 @@ def test_frontend_database_checks_without_a_database(tmp_path):
|
|
|
173
285
|
]
|
|
174
286
|
|
|
175
287
|
|
|
288
|
+
def probe_stub(monkeypatch, output: str, returncode: int = 0):
|
|
289
|
+
monkeypatch.setattr(grade.shutil, 'which', lambda name: '/usr/bin/createdb')
|
|
290
|
+
monkeypatch.setattr(grade, 'run', lambda project, args, env: done(returncode, output))
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@pytest.mark.parametrize('frontend', [False, True])
|
|
294
|
+
@pytest.mark.parametrize('output', ['Traceback: no module', 'not json'])
|
|
295
|
+
def test_database_checks_fail_when_the_server_is_unknown(tmp_path, monkeypatch, frontend, output):
|
|
296
|
+
probe_stub(monkeypatch, output, 1)
|
|
297
|
+
project = product(tmp_path, 1)
|
|
298
|
+
checks = grade.database_checks(project, {}, 'eval_x', frontend)
|
|
299
|
+
reason = 'server of the database: ' + output
|
|
300
|
+
expected = [('migrations apply', False, reason)]
|
|
301
|
+
if frontend:
|
|
302
|
+
expected += [('contract fresh', False, reason), ('end-to-end tests pass', False, reason)]
|
|
303
|
+
assert [(c.name, c.passed, c.detail) for c in checks] == expected
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@pytest.mark.parametrize('frontend', [False, True])
|
|
307
|
+
def test_database_checks_without_createdb(tmp_path, monkeypatch, frontend):
|
|
308
|
+
monkeypatch.setattr(grade.shutil, 'which', lambda name: None)
|
|
309
|
+
monkeypatch.setattr(grade, 'run', lambda *a: pytest.fail('the server must not be probed'))
|
|
310
|
+
checks = grade.database_checks(product(tmp_path, 1), {}, 'eval_x', frontend)
|
|
311
|
+
assert [(c.name, c.passed, c.detail) for c in checks][0] == (
|
|
312
|
+
'migrations apply', False, 'no PostgreSQL (createdb)',
|
|
313
|
+
)
|
|
314
|
+
assert len(checks) == (3 if frontend else 1)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def test_database_checks_create_migrate_and_drop(tmp_path, monkeypatch):
|
|
318
|
+
server = {'host': 'db', 'port': None, 'user': 'owner', 'password': 'secret'}
|
|
319
|
+
probe_stub(monkeypatch, json.dumps(server))
|
|
320
|
+
commands = []
|
|
321
|
+
|
|
322
|
+
def fake_subprocess(command, **kwargs):
|
|
323
|
+
commands.append(command[0])
|
|
324
|
+
return subprocess.CompletedProcess(command, 0, '', '')
|
|
325
|
+
|
|
326
|
+
monkeypatch.setattr(grade.subprocess, 'run', fake_subprocess)
|
|
327
|
+
checks = grade.database_checks(tmp_path, {}, 'eval_x', False)
|
|
328
|
+
assert [(c.name, c.passed) for c in checks] == [('migrations apply', True)]
|
|
329
|
+
assert commands == ['createdb', 'dropdb']
|
|
330
|
+
|
|
331
|
+
|
|
176
332
|
def test_end_to_end_needs_the_test_data(tmp_path, monkeypatch):
|
|
177
333
|
monkeypatch.setattr(grade, 'run', lambda project, args, env: done(1, 'Unknown command: e2e_data'))
|
|
178
334
|
check = grade.end_to_end(product(tmp_path, 1), {})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|