milo-cli 0.3.0__tar.gz → 0.3.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.
Files changed (136) hide show
  1. {milo_cli-0.3.0 → milo_cli-0.3.1}/PKG-INFO +2 -2
  2. {milo_cli-0.3.0 → milo_cli-0.3.1}/pyproject.toml +2 -2
  3. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/__init__.py +1 -1
  4. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/templates/components/command_list.kida +1 -1
  5. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/templates/components/help_page.kida +1 -1
  6. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/templates/error.kida +5 -4
  7. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo_cli.egg-info/PKG-INFO +2 -2
  8. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo_cli.egg-info/SOURCES.txt +1 -0
  9. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo_cli.egg-info/requires.txt +1 -1
  10. milo_cli-0.3.1/tests/test_release_status.py +193 -0
  11. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_templates.py +52 -0
  12. {milo_cli-0.3.0 → milo_cli-0.3.1}/README.md +0 -0
  13. {milo_cli-0.3.0 → milo_cli-0.3.1}/setup.cfg +0 -0
  14. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/_cells.py +0 -0
  15. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/_child.py +0 -0
  16. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/_cli_help.py +0 -0
  17. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/_command_defs.py +0 -0
  18. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/_compat.py +0 -0
  19. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/_errors.py +0 -0
  20. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/_jsonrpc.py +0 -0
  21. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/_mcp_router.py +0 -0
  22. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/_protocols.py +0 -0
  23. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/_scaffold/__init__.py +0 -0
  24. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/_scaffold/default/README.md +0 -0
  25. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/_scaffold/default/app.py +0 -0
  26. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/_scaffold/default/conftest.py +0 -0
  27. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/_scaffold/default/tests/__init__.py +0 -0
  28. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/_scaffold/default/tests/test_app.py +0 -0
  29. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/_types.py +0 -0
  30. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/app.py +0 -0
  31. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/cli.py +0 -0
  32. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/commands.py +0 -0
  33. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/completions.py +0 -0
  34. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/components_cli.py +0 -0
  35. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/config.py +0 -0
  36. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/context.py +0 -0
  37. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/dev.py +0 -0
  38. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/doctor.py +0 -0
  39. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/flow.py +0 -0
  40. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/form.py +0 -0
  41. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/gateway.py +0 -0
  42. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/groups.py +0 -0
  43. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/help.py +0 -0
  44. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/input/__init__.py +0 -0
  45. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/input/_platform.py +0 -0
  46. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/input/_reader.py +0 -0
  47. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/input/_sequences.py +0 -0
  48. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/live.py +0 -0
  49. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/llms.py +0 -0
  50. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/mcp.py +0 -0
  51. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/middleware.py +0 -0
  52. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/observability.py +0 -0
  53. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/output.py +0 -0
  54. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/pipeline.py +0 -0
  55. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/plugins.py +0 -0
  56. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/py.typed +0 -0
  57. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/reducers.py +0 -0
  58. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/registry.py +0 -0
  59. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/schema.py +0 -0
  60. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/state.py +0 -0
  61. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/streaming.py +0 -0
  62. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/templates/__init__.py +0 -0
  63. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/templates/components/_defs.kida +0 -0
  64. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/templates/field_confirm.kida +0 -0
  65. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/templates/field_select.kida +0 -0
  66. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/templates/field_text.kida +0 -0
  67. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/templates/form.kida +0 -0
  68. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/templates/help.kida +0 -0
  69. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/templates/progress.kida +0 -0
  70. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/testing/__init__.py +0 -0
  71. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/testing/_mcp.py +0 -0
  72. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/testing/_record.py +0 -0
  73. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/testing/_replay.py +0 -0
  74. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/testing/_snapshot.py +0 -0
  75. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/theme.py +0 -0
  76. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/verify.py +0 -0
  77. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo/version_check.py +0 -0
  78. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo_cli.egg-info/dependency_links.txt +0 -0
  79. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo_cli.egg-info/entry_points.txt +0 -0
  80. {milo_cli-0.3.0 → milo_cli-0.3.1}/src/milo_cli.egg-info/top_level.txt +0 -0
  81. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_ai_native.py +0 -0
  82. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_app.py +0 -0
  83. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_bubbletea_patterns.py +0 -0
  84. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_child.py +0 -0
  85. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_cli.py +0 -0
  86. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_command_contract.py +0 -0
  87. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_command_defs.py +0 -0
  88. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_commands_core.py +0 -0
  89. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_commands_middleware.py +0 -0
  90. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_compat.py +0 -0
  91. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_completions.py +0 -0
  92. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_components.py +0 -0
  93. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_config.py +0 -0
  94. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_context.py +0 -0
  95. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_dev.py +0 -0
  96. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_docs_information_architecture.py +0 -0
  97. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_docs_snippets.py +0 -0
  98. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_doctor.py +0 -0
  99. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_effects.py +0 -0
  100. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_effects_stress.py +0 -0
  101. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_errors.py +0 -0
  102. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_flow.py +0 -0
  103. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_form.py +0 -0
  104. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_gateway.py +0 -0
  105. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_groups.py +0 -0
  106. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_help.py +0 -0
  107. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_input.py +0 -0
  108. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_lazy.py +0 -0
  109. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_mcp_handler.py +0 -0
  110. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_mcp_prompts.py +0 -0
  111. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_mcp_resources.py +0 -0
  112. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_mcp_router.py +0 -0
  113. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_mcp_transport.py +0 -0
  114. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_middleware.py +0 -0
  115. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_migration_docs.py +0 -0
  116. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_milo_init.py +0 -0
  117. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_mount.py +0 -0
  118. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_observability.py +0 -0
  119. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_outputgallery_example.py +0 -0
  120. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_pipeline.py +0 -0
  121. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_plugins.py +0 -0
  122. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_protocols.py +0 -0
  123. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_readme_example_index.py +0 -0
  124. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_reducers.py +0 -0
  125. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_registry_crud.py +0 -0
  126. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_registry_v2.py +0 -0
  127. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_scaffold.py +0 -0
  128. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_schema_v2.py +0 -0
  129. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_state.py +0 -0
  130. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_streaming.py +0 -0
  131. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_testing.py +0 -0
  132. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_testing_mcp.py +0 -0
  133. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_theme.py +0 -0
  134. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_types.py +0 -0
  135. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_verify.py +0 -0
  136. {milo_cli-0.3.0 → milo_cli-0.3.1}/tests/test_version_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: milo-cli
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Template-driven CLI applications for free-threaded Python
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://lbliii.github.io/milo-cli/
@@ -18,7 +18,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
18
  Classifier: Typing :: Typed
19
19
  Requires-Python: >=3.14
20
20
  Description-Content-Type: text/markdown
21
- Requires-Dist: kida-templates<0.8.0,>=0.7.0
21
+ Requires-Dist: kida-templates<0.10.0,>=0.9.0
22
22
  Provides-Extra: docs
23
23
  Requires-Dist: bengal>=0.3.2; extra == "docs"
24
24
  Provides-Extra: yaml
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "milo-cli"
7
- version = "0.3.0"
7
+ version = "0.3.1"
8
8
  description = "Template-driven CLI applications for free-threaded Python"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14"
11
11
  license = "MIT"
12
- dependencies = ["kida-templates>=0.7.0,<0.8.0"]
12
+ dependencies = ["kida-templates>=0.9.0,<0.10.0"]
13
13
  keywords = ["cli", "terminal", "forms", "free-threading", "template", "elm"]
14
14
  classifiers = [
15
15
  "Development Status :: 3 - Alpha",
@@ -161,7 +161,7 @@ def _Py_mod_gil() -> int: # noqa: N802
161
161
  return 0
162
162
 
163
163
 
164
- __version__ = "0.3.0"
164
+ __version__ = "0.3.1"
165
165
  __all__ = [
166
166
  "BUILTIN_ACTIONS",
167
167
  "CLI",
@@ -5,7 +5,7 @@
5
5
  title -- optional section title (default: "Commands")
6
6
  grouped -- optional bool, if true group by first tag (default: false)
7
7
  -#}
8
- {%- from "components/_defs.kida" import section, command_row, tag_list -%}
8
+ {%- from "./_defs.kida" import section, command_row, tag_list -%}
9
9
 
10
10
  {{ section(title | default("Commands")) }}
11
11
  {% for cmd in commands %}
@@ -10,7 +10,7 @@
10
10
  examples -- list of example dicts: {command, description} (optional)
11
11
  epilog -- footer text (optional)
12
12
  -#}
13
- {%- from "components/_defs.kida" import header, section, command_row, example_block, def_list -%}
13
+ {%- from "./_defs.kida" import header, section, command_row, example_block, def_list -%}
14
14
 
15
15
  {% set version ??= "" %}
16
16
  {% set description ??= "" %}
@@ -1,9 +1,10 @@
1
- {% try %}
1
+ {%- set error ??= "Unknown error" -%}
2
+ {%- set code ??= "" -%}
3
+ {%- set template_name ??= "" -%}
4
+ {%- set hint ??= "" -%}
5
+ {%- set docs_url ??= "" -%}
2
6
  {{ "error" | red | bold }}{% if code %} {{ code | yellow }}{% endif %}
3
7
  {{ error | dim }}
4
8
  {% if template_name %} {{ "template:" | dim }} {{ template_name }}{% endif %}
5
9
  {% if hint %} {{ "hint:" | cyan }} {{ hint }}{% endif %}
6
10
  {% if docs_url %} {{ "docs:" | dim }} {{ docs_url }}{% endif %}
7
- {% fallback %}
8
- error: {{ error }}
9
- {% end %}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: milo-cli
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Template-driven CLI applications for free-threaded Python
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://lbliii.github.io/milo-cli/
@@ -18,7 +18,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
18
  Classifier: Typing :: Typed
19
19
  Requires-Python: >=3.14
20
20
  Description-Content-Type: text/markdown
21
- Requires-Dist: kida-templates<0.8.0,>=0.7.0
21
+ Requires-Dist: kida-templates<0.10.0,>=0.9.0
22
22
  Provides-Extra: docs
23
23
  Requires-Dist: bengal>=0.3.2; extra == "docs"
24
24
  Provides-Extra: yaml
@@ -120,6 +120,7 @@ tests/test_readme_example_index.py
120
120
  tests/test_reducers.py
121
121
  tests/test_registry_crud.py
122
122
  tests/test_registry_v2.py
123
+ tests/test_release_status.py
123
124
  tests/test_scaffold.py
124
125
  tests/test_schema_v2.py
125
126
  tests/test_state.py
@@ -1,4 +1,4 @@
1
- kida-templates<0.8.0,>=0.7.0
1
+ kida-templates<0.10.0,>=0.9.0
2
2
 
3
3
  [docs]
4
4
  bengal>=0.3.2
@@ -0,0 +1,193 @@
1
+ """Tests for scripts/release_status.py."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import sys
7
+ from pathlib import Path
8
+
9
+
10
+ def _load_release_status_script():
11
+ script = Path(__file__).parent.parent / "scripts" / "release_status.py"
12
+ spec = importlib.util.spec_from_file_location("release_status", script)
13
+ assert spec is not None
14
+ assert spec.loader is not None
15
+ module = importlib.util.module_from_spec(spec)
16
+ sys.modules[spec.name] = module
17
+ spec.loader.exec_module(module)
18
+ return module
19
+
20
+
21
+ def _facts(**overrides):
22
+ release_status = _load_release_status_script()
23
+ base = {
24
+ "package_name": "milo-cli",
25
+ "pyproject_version": "0.3.1",
26
+ "module_version": "0.3.1",
27
+ "lock_version": "0.3.1",
28
+ "head_sha": "abc1234",
29
+ "local_tags": ("v0.3.0",),
30
+ "remote_tags": ("refs/tags/v0.3.0",),
31
+ "pypi_version": "",
32
+ "changelog_versions": ("0.3.1", "0.3.0"),
33
+ "release_note_versions": ("0.3.1", "0.3.0"),
34
+ "pending_fragments": (),
35
+ "empty_fragments": (),
36
+ "changed_since_current_tag": (),
37
+ "changed_released_notes": (),
38
+ "tag_diff_warnings": (),
39
+ "remote_checked": True,
40
+ "pypi_checked": False,
41
+ }
42
+ base.update(overrides)
43
+ return release_status.ReleaseFacts(**base)
44
+
45
+
46
+ class TestReleaseStatus:
47
+ def test_allows_next_patch_after_released_tag(self):
48
+ release_status = _load_release_status_script()
49
+
50
+ report = release_status.analyze_release_state(_facts())
51
+
52
+ messages = [check.message for check in report.checks]
53
+ assert report.has_errors is False
54
+ assert report.latest_released_version == "0.3.0"
55
+ assert report.suggested_next_version == "0.3.1"
56
+ assert any("Current version 0.3.1 is newer" in msg for msg in messages)
57
+
58
+ def test_rejects_metadata_left_on_already_released_version(self):
59
+ release_status = _load_release_status_script()
60
+
61
+ report = release_status.analyze_release_state(
62
+ _facts(
63
+ pyproject_version="0.3.0",
64
+ module_version="0.3.0",
65
+ lock_version="0.3.0",
66
+ changelog_versions=("0.3.0",),
67
+ release_note_versions=("0.3.0",),
68
+ changed_since_current_tag=("CHANGELOG.md", "site/content/releases/0.3.0.md"),
69
+ )
70
+ )
71
+
72
+ messages = [check.message for check in report.checks]
73
+ assert report.has_errors is True
74
+ assert any("Version 0.3.0 is already released/tagged" in msg for msg in messages)
75
+ assert any("Release surfaces for already-tagged 0.3.0 changed" in msg for msg in messages)
76
+ assert report.suggested_next_version == "0.3.1"
77
+
78
+ def test_rejects_version_drift_between_metadata_files(self):
79
+ release_status = _load_release_status_script()
80
+
81
+ report = release_status.analyze_release_state(_facts(module_version="0.3.0"))
82
+
83
+ assert report.has_errors is True
84
+ assert any("disagrees with src/milo/__init__.py" in c.message for c in report.checks)
85
+
86
+ def test_rejects_version_already_on_pypi(self):
87
+ release_status = _load_release_status_script()
88
+
89
+ report = release_status.analyze_release_state(
90
+ _facts(
91
+ local_tags=(),
92
+ remote_tags=(),
93
+ pypi_version="0.3.1",
94
+ pypi_checked=True,
95
+ )
96
+ )
97
+
98
+ assert report.has_errors is True
99
+ assert any("Version 0.3.1 is already released/tagged" in c.message for c in report.checks)
100
+
101
+ def test_warns_about_pending_fragments(self):
102
+ release_status = _load_release_status_script()
103
+
104
+ report = release_status.analyze_release_state(
105
+ _facts(pending_fragments=("template-check.fixed.md",))
106
+ )
107
+
108
+ assert any(
109
+ check.level == "warn" and "Pending changelog fragments" in check.message
110
+ for check in report.checks
111
+ )
112
+
113
+ def test_rejects_empty_changelog_fragments(self):
114
+ release_status = _load_release_status_script()
115
+
116
+ report = release_status.analyze_release_state(
117
+ _facts(
118
+ pending_fragments=("empty.fixed.md",),
119
+ empty_fragments=("empty.fixed.md",),
120
+ )
121
+ )
122
+
123
+ assert report.has_errors is True
124
+ assert any("Empty changelog fragments found" in c.message for c in report.checks)
125
+
126
+ def test_warns_when_prior_release_notes_changed(self):
127
+ release_status = _load_release_status_script()
128
+
129
+ report = release_status.analyze_release_state(
130
+ _facts(changed_released_notes=("site/content/releases/0.3.0.md",))
131
+ )
132
+
133
+ assert any(
134
+ check.level == "warn" and "Already-tagged release note files changed" in check.message
135
+ for check in report.checks
136
+ )
137
+
138
+ def test_warns_when_tag_diff_cannot_be_checked(self):
139
+ release_status = _load_release_status_script()
140
+
141
+ report = release_status.analyze_release_state(
142
+ _facts(tag_diff_warnings=("Release tag v0.3.0 exists on origin but is not local.",))
143
+ )
144
+
145
+ assert any(
146
+ check.level == "warn" and "exists on origin" in check.message for check in report.checks
147
+ )
148
+
149
+ def test_remote_only_current_tag_reports_missing_local_diff(self, tmp_path):
150
+ release_status = _load_release_status_script()
151
+
152
+ changed, warnings = release_status._read_changed_since_current_tag(
153
+ tmp_path,
154
+ "0.3.1",
155
+ (),
156
+ ("refs/tags/v0.3.1",),
157
+ )
158
+
159
+ assert changed == ()
160
+ assert warnings == (
161
+ "Release tag v0.3.1 exists on origin but is not available locally; "
162
+ "run with --fetch-tags to check release-surface drift.",
163
+ )
164
+
165
+ def test_remote_only_historical_tag_reports_missing_local_diff(self, tmp_path):
166
+ release_status = _load_release_status_script()
167
+ note = tmp_path / "site" / "content" / "releases" / "0.3.0.md"
168
+ note.parent.mkdir(parents=True)
169
+ note.write_text("release notes\n", encoding="utf-8")
170
+
171
+ changed, warnings = release_status._read_changed_released_notes(
172
+ tmp_path,
173
+ "0.3.1",
174
+ (),
175
+ ("refs/tags/v0.3.0",),
176
+ )
177
+
178
+ assert changed == ()
179
+ assert warnings == (
180
+ "Release tag v0.3.0 exists on origin but is not available locally; "
181
+ "run with --fetch-tags to check site/content/releases/0.3.0.md "
182
+ "for historical edits.",
183
+ )
184
+
185
+ def test_reads_empty_fragments_from_disk(self, tmp_path):
186
+ release_status = _load_release_status_script()
187
+ fragment_dir = tmp_path / "changelog.d"
188
+ fragment_dir.mkdir()
189
+ (fragment_dir / ".gitkeep").write_text("", encoding="utf-8")
190
+ (fragment_dir / "empty.fixed.md").write_text(" \n", encoding="utf-8")
191
+ (fragment_dir / "full.fixed.md").write_text("Fixed the release guard.\n", encoding="utf-8")
192
+
193
+ assert release_status._read_empty_fragments(tmp_path) == ("empty.fixed.md",)
@@ -2,6 +2,19 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import importlib.util
6
+ from pathlib import Path
7
+
8
+
9
+ def _load_check_templates_script():
10
+ script = Path(__file__).parent.parent / "scripts" / "check_templates.py"
11
+ spec = importlib.util.spec_from_file_location("check_templates", script)
12
+ assert spec is not None
13
+ assert spec.loader is not None
14
+ module = importlib.util.module_from_spec(spec)
15
+ spec.loader.exec_module(module)
16
+ return module
17
+
5
18
 
6
19
  class TestGetEnv:
7
20
  def test_returns_environment(self):
@@ -203,3 +216,42 @@ class TestComponentTemplatesIncluded:
203
216
  except Exception:
204
217
  # If kida can't parse it, that's OK -- the point is the file is found
205
218
  pass
219
+
220
+
221
+ class TestTemplateCheckScript:
222
+ def test_milo_aware_check_accepts_terminal_filters(self, tmp_path):
223
+ """The local checker uses Milo's terminal environment, not raw kida defaults."""
224
+ check_templates = _load_check_templates_script()
225
+ (tmp_path / "ok.kida").write_text('{{ "ok" | bold }}', encoding="utf-8")
226
+
227
+ errors = check_templates._check_root(tmp_path, "tmp")
228
+
229
+ assert errors == []
230
+
231
+ def test_milo_aware_check_flags_unified_end_tags(self, tmp_path):
232
+ check_templates = _load_check_templates_script()
233
+ (tmp_path / "bad.kida").write_text("{% if value %}ok{% end %}", encoding="utf-8")
234
+
235
+ errors = check_templates._check_root(tmp_path, "tmp")
236
+
237
+ assert len(errors) == 1
238
+ assert "bad.kida:1" in errors[0]
239
+ assert "strict: unified {% end %} closes 'if'" in errors[0]
240
+ assert "prefer {% endif %}" in errors[0]
241
+
242
+ def test_milo_aware_check_flags_fragile_same_folder_imports(self, tmp_path):
243
+ check_templates = _load_check_templates_script()
244
+ components = tmp_path / "components"
245
+ components.mkdir()
246
+ (components / "_defs.kida").write_text("{% def item() %}x{% enddef %}", encoding="utf-8")
247
+ (components / "page.kida").write_text(
248
+ '{% from "components/_defs.kida" import item %}{{ item() }}',
249
+ encoding="utf-8",
250
+ )
251
+
252
+ errors = check_templates._check_root(tmp_path, "tmp")
253
+
254
+ assert len(errors) == 1
255
+ assert "components/page.kida:1" in errors[0]
256
+ assert "lint/fragile-path" in errors[0]
257
+ assert 'prefer "./_defs.kida"' in errors[0]
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