strictcli 0.31.0__tar.gz → 0.32.0__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.
- strictcli-0.32.0/CLAUDE.md +26 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/PKG-INFO +1 -1
- {strictcli-0.31.0 → strictcli-0.32.0}/pyproject.toml +1 -1
- {strictcli-0.31.0 → strictcli-0.32.0}/strictcli/__init__.py +67 -22
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_coverage.py +102 -6
- {strictcli-0.31.0 → strictcli-0.32.0}/uv.lock +1 -1
- strictcli-0.31.0/CLAUDE.md +0 -20
- {strictcli-0.31.0 → strictcli-0.32.0}/.claude/settings.json +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/.github/workflows/ci.yml +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/.github/workflows/publish.yml +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/.gitignore +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/.rlsbl/bases/.gitignore +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/.rlsbl/config.json +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/.rlsbl/managed-files.json +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/.rlsbl/version +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/.strictcli/schema.json +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/LICENSE +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/README.md +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/strictcli/py.typed +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/conftest.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_arg_default.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_arg_default_validation.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_at_prefix.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_auto_version.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_call.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_check_command.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_check_discovery.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_check_provider.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_check_public_api.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_check_runner.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_check_schema.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_check_types.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_choices.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_choices_none.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_command_tags.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_compound_types.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_config.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_config_fields.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_config_file_path.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_config_set_bugs.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_context.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_dependencies.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_deprecated.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_dump_schema.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_e2e.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_env.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_exit_codes.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_flag_sets.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_float_format.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_float_type.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_float_vectors.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_global_flag_conflict_position.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_global_flags.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_help.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_hermetic.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_infra_env.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_int_type.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_invoke.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_keyword_flags.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_mcp.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_mutex.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_nesting.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_parser.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_passthrough.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_provenance.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_provenance_phase2.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_registration.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_repeatable.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_reserved_global_flags.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_tagdsl.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_toml_loading.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_tool_export.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_typed_args.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_unique.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_utilities.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_validate.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_variadic.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/tests/test_visibility.py +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/todo/.defer/deferred.md +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/todo/.done/keyword-collision-in-flag-param-name.md +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/todo/.done/original-idea.md +0 -0
- {strictcli-0.31.0 → strictcli-0.32.0}/todo/.done/public-check-runner-api.md +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# strictcli (Python)
|
|
2
|
+
|
|
3
|
+
Python implementation of strictcli. Published to PyPI. Part of the strictcli
|
|
4
|
+
rlsbl monorepo as releasable `py-strictcli`.
|
|
5
|
+
|
|
6
|
+
## Development
|
|
7
|
+
|
|
8
|
+
- `uv sync` then `uv run pytest` from this directory. Always use `uv`, never pip.
|
|
9
|
+
- Run the conformance suite after changes: `cd ../conformance && python run.py --target python`.
|
|
10
|
+
- CI (`ci-router.yml` at the repo root) runs the full pytest suite on every push touching `python/**`.
|
|
11
|
+
|
|
12
|
+
## Release workflow
|
|
13
|
+
|
|
14
|
+
Releases go through [rlsbl](https://github.com/smm-h/rlsbl) monorepo releases.
|
|
15
|
+
|
|
16
|
+
- CHANGELOG.md is generated from JSONL changelog entries — never edit it by hand.
|
|
17
|
+
Add entries with `rlsbl changelog add` from inside `python/` after each commit
|
|
18
|
+
(they land in `.rlsbl-monorepo/releasables/py-strictcli/changes/unreleased.jsonl`).
|
|
19
|
+
- To release: from the repo root, run `rlsbl monorepo release init`, edit the
|
|
20
|
+
scaffolded release file (bump type, description, and context live in the file),
|
|
21
|
+
then run `rlsbl monorepo release run --no-allow-dirty --watch --yes`.
|
|
22
|
+
- Never publish or push manually — CI publishes to PyPI via Trusted Publishing.
|
|
23
|
+
|
|
24
|
+
## Conventions
|
|
25
|
+
|
|
26
|
+
- No tokens or secrets in command-line arguments (use env vars or config files).
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
__version__ = "0.
|
|
5
|
+
__version__ = "0.32.0"
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"App", "Flag", "Arg", "FlagSet", "MutexGroup", "CoRequired", "Requires",
|
|
@@ -2803,13 +2803,25 @@ class App:
|
|
|
2803
2803
|
# that every command in the app's surface has been exercised.
|
|
2804
2804
|
self._coverage_shard_counter: int = 0
|
|
2805
2805
|
self._coverage_shard_path: str | None = None
|
|
2806
|
+
self._coverage_dir: str | None = None
|
|
2807
|
+
self._coverage_manifest_path: str | None = None
|
|
2806
2808
|
self._last_resolved_path: list[str] = []
|
|
2807
2809
|
if self.test_coverage:
|
|
2810
|
+
# Anchor the coverage root to the cwd AT CONSTRUCTION TIME. Both the
|
|
2811
|
+
# recorder and the check provider use these absolute paths so that
|
|
2812
|
+
# tests which chdir still record into the repo, and a check evaluated
|
|
2813
|
+
# from a foreign cwd reads the app's own repo state.
|
|
2814
|
+
self._coverage_dir = os.path.abspath(
|
|
2815
|
+
os.path.join(".strictcli", "coverage")
|
|
2816
|
+
)
|
|
2817
|
+
self._coverage_manifest_path = os.path.abspath(
|
|
2818
|
+
os.path.join(".strictcli", "test-coverage.json")
|
|
2819
|
+
)
|
|
2808
2820
|
self._coverage_shard_path = os.path.join(
|
|
2809
|
-
|
|
2821
|
+
self._coverage_dir,
|
|
2810
2822
|
f"{os.getpid()}-{{n}}.jsonl",
|
|
2811
2823
|
)
|
|
2812
|
-
os.makedirs(
|
|
2824
|
+
os.makedirs(self._coverage_dir, exist_ok=True)
|
|
2813
2825
|
self.register_check_provider(self._test_coverage_provider)
|
|
2814
2826
|
|
|
2815
2827
|
def _validate_flag_infra_marker(self, f: Flag) -> None:
|
|
@@ -2868,18 +2880,46 @@ class App:
|
|
|
2868
2880
|
def _test_coverage_provider(self) -> list[CheckSpec]:
|
|
2869
2881
|
"""Built-in check provider for cli-test-coverage.
|
|
2870
2882
|
|
|
2871
|
-
Registered automatically when test_coverage=True.
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2883
|
+
Registered automatically when test_coverage=True. The verdict is derived
|
|
2884
|
+
from committed state: the covered set is the union of the committed
|
|
2885
|
+
manifest (.strictcli/test-coverage.json) and any per-process shard files
|
|
2886
|
+
merged from .strictcli/coverage/. Every live registered command path
|
|
2887
|
+
(minus the injected check command) must be present in that union to pass;
|
|
2888
|
+
otherwise the check fails naming each uncovered command.
|
|
2889
|
+
|
|
2890
|
+
Because the verdict reads the committed manifest, it is deterministic on
|
|
2891
|
+
every machine -- a machine that never ran the suite (no local shards)
|
|
2892
|
+
still gets a stable verdict from the committed manifest alone. Both the
|
|
2893
|
+
coverage dir and the manifest path are anchored to the App's
|
|
2894
|
+
construction-time cwd, so the check evaluated from a foreign cwd reads
|
|
2895
|
+
the app's own repo state.
|
|
2896
|
+
|
|
2897
|
+
The manifest is rewritten as the monotonic union of its prior contents
|
|
2898
|
+
and the freshly merged shards, but ONLY when that content actually
|
|
2899
|
+
changes -- a pure check must not dirty a byte-identical file. Accepted
|
|
2900
|
+
staleness: deleting a test leaves its command covered in the manifest
|
|
2901
|
+
until the manifest is deliberately regenerated (e.g. by removing it and
|
|
2902
|
+
re-running the suite), because the union never removes a command.
|
|
2875
2903
|
"""
|
|
2876
2904
|
def impl(ctx: CheckContext, reporter: "ErrorReporter") -> "_CheckOutcome":
|
|
2877
|
-
coverage_dir =
|
|
2878
|
-
manifest_path =
|
|
2905
|
+
coverage_dir = self._coverage_dir
|
|
2906
|
+
manifest_path = self._coverage_manifest_path
|
|
2879
2907
|
|
|
2880
|
-
# Merge shards
|
|
2881
2908
|
covered: set[str] = set()
|
|
2882
|
-
|
|
2909
|
+
|
|
2910
|
+
# Seed from the committed manifest -- this is what makes the verdict
|
|
2911
|
+
# deterministic on machines that never ran the suite.
|
|
2912
|
+
if manifest_path and os.path.isfile(manifest_path):
|
|
2913
|
+
try:
|
|
2914
|
+
with open(manifest_path, encoding="utf-8") as f:
|
|
2915
|
+
data = json.load(f)
|
|
2916
|
+
if isinstance(data, list):
|
|
2917
|
+
covered.update(c for c in data if isinstance(c, str))
|
|
2918
|
+
except (json.JSONDecodeError, OSError):
|
|
2919
|
+
pass
|
|
2920
|
+
|
|
2921
|
+
# Merge shards (optional freshness input)
|
|
2922
|
+
if coverage_dir and os.path.isdir(coverage_dir):
|
|
2883
2923
|
for fname in os.listdir(coverage_dir):
|
|
2884
2924
|
if not fname.endswith(".jsonl"):
|
|
2885
2925
|
continue
|
|
@@ -2893,17 +2933,22 @@ class App:
|
|
|
2893
2933
|
if "command" in entry:
|
|
2894
2934
|
covered.add(entry["command"])
|
|
2895
2935
|
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
)
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2936
|
+
# Rewrite the manifest as the monotonic union, but only when the
|
|
2937
|
+
# content actually changes (keeps a pure check from dirtying a
|
|
2938
|
+
# byte-identical file).
|
|
2939
|
+
if manifest_path and covered:
|
|
2940
|
+
new_content = json.dumps(sorted(covered), indent=2) + "\n"
|
|
2941
|
+
existing: str | None = None
|
|
2942
|
+
if os.path.isfile(manifest_path):
|
|
2943
|
+
try:
|
|
2944
|
+
with open(manifest_path, encoding="utf-8") as f:
|
|
2945
|
+
existing = f.read()
|
|
2946
|
+
except OSError:
|
|
2947
|
+
existing = None
|
|
2948
|
+
if existing != new_content:
|
|
2949
|
+
os.makedirs(os.path.dirname(manifest_path), exist_ok=True)
|
|
2950
|
+
with open(manifest_path, "w", encoding="utf-8") as f:
|
|
2951
|
+
f.write(new_content)
|
|
2907
2952
|
|
|
2908
2953
|
# Compare against command surface (exclude the framework-injected
|
|
2909
2954
|
# check command -- it is not a user command)
|
|
@@ -175,10 +175,15 @@ class TestCoverageCheck:
|
|
|
175
175
|
cov_result = next(r for r in results if r.name == "cli-test-coverage")
|
|
176
176
|
assert cov_result.status == "pass"
|
|
177
177
|
|
|
178
|
-
def
|
|
179
|
-
"""No
|
|
178
|
+
def test_empty_coverage_fails_naming_all(self, tmp_path):
|
|
179
|
+
"""No shards and no manifest -> deterministic FAIL naming every command.
|
|
180
|
+
|
|
181
|
+
The verdict is derived from committed state (manifest union shards), so an
|
|
182
|
+
empty everything state fails by listing every uncovered command rather than
|
|
183
|
+
the old machine-dependent "no coverage data" error.
|
|
184
|
+
"""
|
|
180
185
|
app = _make_app(tmp_path)
|
|
181
|
-
# Don't run any test() or call() -- no shards
|
|
186
|
+
# Don't run any test() or call() -- no shards, no manifest
|
|
182
187
|
|
|
183
188
|
results, _, code = app.run_checks(
|
|
184
189
|
SimpleCtx(project_root=tmp_path),
|
|
@@ -186,9 +191,13 @@ class TestCoverageCheck:
|
|
|
186
191
|
)
|
|
187
192
|
cov_result = next(r for r in results if r.name == "cli-test-coverage")
|
|
188
193
|
assert cov_result.status == "fail"
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
194
|
+
problem_texts = [p.text for p in cov_result.problems]
|
|
195
|
+
uncovered = {
|
|
196
|
+
t.split("no test coverage for command: ")[1]
|
|
197
|
+
for t in problem_texts
|
|
198
|
+
if "no test coverage for command:" in t
|
|
199
|
+
}
|
|
200
|
+
assert uncovered == {"build", "deploy", "status"}
|
|
192
201
|
|
|
193
202
|
def test_manifest_written_on_check(self, tmp_path):
|
|
194
203
|
"""The check writes .strictcli/test-coverage.json with covered commands."""
|
|
@@ -239,6 +248,93 @@ class TestCoverageCheck:
|
|
|
239
248
|
assert any("infra.teardown" in t for t in problem_texts)
|
|
240
249
|
|
|
241
250
|
|
|
251
|
+
class TestCoverageChdirSafety:
|
|
252
|
+
def test_record_anchored_to_construction_cwd(self, tmp_path):
|
|
253
|
+
"""test() records into the construction-time coverage dir, not the cwd
|
|
254
|
+
that a test happened to chdir into."""
|
|
255
|
+
app = _make_app(tmp_path) # constructs with cwd == tmp_path
|
|
256
|
+
other = tmp_path / "elsewhere"
|
|
257
|
+
other.mkdir()
|
|
258
|
+
os.chdir(other)
|
|
259
|
+
|
|
260
|
+
app.test(["deploy"])
|
|
261
|
+
|
|
262
|
+
construction_shards = list(
|
|
263
|
+
(tmp_path / ".strictcli" / "coverage").glob("*.jsonl")
|
|
264
|
+
)
|
|
265
|
+
assert construction_shards, "shard must land under the construction cwd"
|
|
266
|
+
foreign = other / ".strictcli" / "coverage"
|
|
267
|
+
assert not foreign.exists(), "must not record into the chdir'd cwd"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class TestManifestUnionVerdict:
|
|
271
|
+
def test_manifest_present_shards_absent_passes(self, tmp_path):
|
|
272
|
+
"""Committed manifest covering every command -> deterministic PASS even
|
|
273
|
+
with no shard files (the machine never ran the suite)."""
|
|
274
|
+
app = _make_app(tmp_path)
|
|
275
|
+
manifest_path = tmp_path / ".strictcli" / "test-coverage.json"
|
|
276
|
+
manifest_path.write_text(
|
|
277
|
+
json.dumps(["build", "deploy", "status"], indent=2) + "\n"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
results, _, code = app.run_checks(
|
|
281
|
+
SimpleCtx(project_root=tmp_path),
|
|
282
|
+
run_all=True,
|
|
283
|
+
)
|
|
284
|
+
cov_result = next(r for r in results if r.name == "cli-test-coverage")
|
|
285
|
+
assert cov_result.status == "pass"
|
|
286
|
+
|
|
287
|
+
def test_check_from_foreign_cwd_reads_app_state(self, tmp_path):
|
|
288
|
+
"""The check evaluated from a foreign cwd reads the app's own repo state
|
|
289
|
+
(anchored manifest), not the foreign directory."""
|
|
290
|
+
app = _make_app(tmp_path)
|
|
291
|
+
manifest_path = tmp_path / ".strictcli" / "test-coverage.json"
|
|
292
|
+
manifest_path.write_text(
|
|
293
|
+
json.dumps(["build", "deploy", "status"], indent=2) + "\n"
|
|
294
|
+
)
|
|
295
|
+
other = tmp_path / "foreign"
|
|
296
|
+
other.mkdir()
|
|
297
|
+
os.chdir(other)
|
|
298
|
+
|
|
299
|
+
results, _, code = app.run_checks(
|
|
300
|
+
SimpleCtx(project_root=tmp_path),
|
|
301
|
+
run_all=True,
|
|
302
|
+
)
|
|
303
|
+
cov_result = next(r for r in results if r.name == "cli-test-coverage")
|
|
304
|
+
assert cov_result.status == "pass"
|
|
305
|
+
|
|
306
|
+
def test_manifest_union_is_monotonic(self, tmp_path):
|
|
307
|
+
"""A run recording only a subset keeps prior commands covered (union)."""
|
|
308
|
+
app = _make_app(tmp_path)
|
|
309
|
+
manifest_path = tmp_path / ".strictcli" / "test-coverage.json"
|
|
310
|
+
manifest_path.write_text(
|
|
311
|
+
json.dumps(["build", "deploy", "status"], indent=2) + "\n"
|
|
312
|
+
)
|
|
313
|
+
app.test(["deploy"]) # only one recorded this run
|
|
314
|
+
|
|
315
|
+
app.run_checks(SimpleCtx(project_root=tmp_path), run_all=True)
|
|
316
|
+
|
|
317
|
+
manifest = json.loads(manifest_path.read_text())
|
|
318
|
+
assert sorted(manifest) == ["build", "deploy", "status"]
|
|
319
|
+
|
|
320
|
+
def test_manifest_not_rewritten_when_unchanged(self, tmp_path):
|
|
321
|
+
"""A pure check must not dirty a byte-identical manifest."""
|
|
322
|
+
app = _make_app(tmp_path)
|
|
323
|
+
app.test(["deploy"])
|
|
324
|
+
app.test(["status"])
|
|
325
|
+
app.test(["build"])
|
|
326
|
+
app.run_checks(SimpleCtx(project_root=tmp_path), run_all=True)
|
|
327
|
+
|
|
328
|
+
manifest_path = tmp_path / ".strictcli" / "test-coverage.json"
|
|
329
|
+
content1 = manifest_path.read_text()
|
|
330
|
+
mtime1 = manifest_path.stat().st_mtime_ns
|
|
331
|
+
|
|
332
|
+
app.run_checks(SimpleCtx(project_root=tmp_path), run_all=True)
|
|
333
|
+
|
|
334
|
+
assert manifest_path.read_text() == content1
|
|
335
|
+
assert manifest_path.stat().st_mtime_ns == mtime1
|
|
336
|
+
|
|
337
|
+
|
|
242
338
|
class TestCoverageDisabled:
|
|
243
339
|
def test_no_recording_when_disabled(self, tmp_path):
|
|
244
340
|
"""test_coverage=False (default) produces no shard files."""
|
strictcli-0.31.0/CLAUDE.md
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# strictcli
|
|
2
|
-
|
|
3
|
-
## Release workflow
|
|
4
|
-
|
|
5
|
-
This project uses [rlsbl](https://github.com/smm-h/rlsbl) for release orchestration.
|
|
6
|
-
|
|
7
|
-
- Update CHANGELOG.md with a `## X.Y.Z` entry describing changes
|
|
8
|
-
- Run `rlsbl release [patch|minor|major]` to bump version and create a GitHub Release
|
|
9
|
-
- CI handles publishing automatically via the publish workflow
|
|
10
|
-
- Never publish manually — always use `rlsbl release`
|
|
11
|
-
- Configure Trusted Publishing on pypi.org for automated PyPI releases
|
|
12
|
-
- Use `rlsbl release --dry-run` to preview a release without making changes
|
|
13
|
-
|
|
14
|
-
## Conventions
|
|
15
|
-
|
|
16
|
-
- No tokens or secrets in command-line arguments (use env vars or config files)
|
|
17
|
-
- All file writes to shared state should be atomic (write to tmp, then rename)
|
|
18
|
-
- External calls (APIs, CLI tools) must have timeouts and graceful fallbacks
|
|
19
|
-
- Use `uv pip install -e .` for local development
|
|
20
|
-
- CI runs smoke tests on every push; manual testing for UI/UX 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
|
|
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
|
|
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
|
|
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
|