Implemented the next missing parity slice around setup-time runtime checks.
This commit is contained in:
+2
-2
@@ -175,10 +175,10 @@ Missing:
|
|||||||
- [ ] Self-hosted runner mode
|
- [ ] Self-hosted runner mode
|
||||||
- [ ] tmux fast paths
|
- [ ] tmux fast paths
|
||||||
- [ ] Worktree fast paths at the CLI entrypoint level
|
- [ ] Worktree fast paths at the CLI entrypoint level
|
||||||
- [ ] Node.js version check and platform setup from `setup.ts`
|
- [x] Python (Node.js equivalent) version check and platform detection from `setup.ts`
|
||||||
- [ ] Worktree creation/setup from `setup.ts`
|
- [ ] Worktree creation/setup from `setup.ts`
|
||||||
- [ ] Terminal backup/restore from `setup.ts`
|
- [ ] Terminal backup/restore from `setup.ts`
|
||||||
- [ ] Release notes checking from `setup.ts`
|
- [x] Release notes checking from `setup.ts` (local CHANGELOG.md, no network/cache layer)
|
||||||
- [ ] Full `entrypoints/cli.tsx` parity (version flag, feature flags, env setup, dynamic imports)
|
- [ ] Full `entrypoints/cli.tsx` parity (version flag, feature flags, env setup, dynamic imports)
|
||||||
- [ ] Full `entrypoints/init.ts` parity (settings validation, OAuth, policy limits, telemetry, cleanup handlers)
|
- [ ] Full `entrypoints/init.ts` parity (settings validation, OAuth, policy limits, telemetry, cleanup handlers)
|
||||||
- [ ] SDK entrypoint (`entrypoints/sdk/` — controlTypes, coreTypes, runtimeTypes, settingsTypes, toolTypes)
|
- [ ] SDK entrypoint (`entrypoints/sdk/` — controlTypes, coreTypes, runtimeTypes, settingsTypes, toolTypes)
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"""Local release-notes parsing — Python port of utils/releaseNotes.ts.
|
||||||
|
|
||||||
|
The Python runtime has no network/cache layer, so this module only reads a
|
||||||
|
local CHANGELOG.md (typically in the project root). The npm version fetches
|
||||||
|
from GitHub and caches under ~/.claude/cache/changelog.md; here, callers
|
||||||
|
provide the changelog text or pass the project cwd so we can read it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
MAX_RELEASE_NOTES_SHOWN = 5
|
||||||
|
|
||||||
|
|
||||||
|
def parse_changelog(content: str) -> dict[str, list[str]]:
|
||||||
|
"""Parse a markdown CHANGELOG into {version: [bullet, ...]}.
|
||||||
|
|
||||||
|
Recognises sections starting with `## <version>` (optionally followed by
|
||||||
|
` - YYYY-MM-DD`). Bullet lines starting with `- ` become entries.
|
||||||
|
"""
|
||||||
|
if not content:
|
||||||
|
return {}
|
||||||
|
notes: dict[str, list[str]] = {}
|
||||||
|
sections = re.split(r'^## ', content, flags=re.MULTILINE)[1:]
|
||||||
|
for section in sections:
|
||||||
|
lines = section.strip().splitlines()
|
||||||
|
if not lines:
|
||||||
|
continue
|
||||||
|
version = lines[0].split(' - ')[0].strip()
|
||||||
|
if not version:
|
||||||
|
continue
|
||||||
|
bullets: list[str] = []
|
||||||
|
for line in lines[1:]:
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith('- '):
|
||||||
|
text = stripped[2:].strip()
|
||||||
|
if text:
|
||||||
|
bullets.append(text)
|
||||||
|
if bullets:
|
||||||
|
notes[version] = bullets
|
||||||
|
return notes
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_version(value: str | None) -> tuple[int, ...] | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
match = re.match(r'^(\d+)(?:\.(\d+))?(?:\.(\d+))?', value.strip())
|
||||||
|
if match is None:
|
||||||
|
return None
|
||||||
|
return tuple(int(part) if part else 0 for part in match.groups())
|
||||||
|
|
||||||
|
|
||||||
|
def _gt(a: str, b: str) -> bool:
|
||||||
|
parsed_a = _coerce_version(a)
|
||||||
|
parsed_b = _coerce_version(b)
|
||||||
|
if parsed_a is None or parsed_b is None:
|
||||||
|
return False
|
||||||
|
return parsed_a > parsed_b
|
||||||
|
|
||||||
|
|
||||||
|
def get_recent_release_notes(
|
||||||
|
current_version: str,
|
||||||
|
previous_version: str | None,
|
||||||
|
changelog_content: str,
|
||||||
|
) -> list[str]:
|
||||||
|
"""Return up to MAX_RELEASE_NOTES_SHOWN bullets newer than previous_version."""
|
||||||
|
notes = parse_changelog(changelog_content)
|
||||||
|
base_current = _coerce_version(current_version)
|
||||||
|
base_previous = _coerce_version(previous_version)
|
||||||
|
if base_previous is not None and base_current is not None and base_current <= base_previous:
|
||||||
|
return []
|
||||||
|
relevant = [
|
||||||
|
(version, bullets)
|
||||||
|
for version, bullets in notes.items()
|
||||||
|
if base_previous is None or _gt(version, previous_version or '0')
|
||||||
|
]
|
||||||
|
relevant.sort(key=lambda item: _coerce_version(item[0]) or (), reverse=True)
|
||||||
|
flat: list[str] = []
|
||||||
|
for _, bullets in relevant:
|
||||||
|
flat.extend(bullets)
|
||||||
|
return flat[:MAX_RELEASE_NOTES_SHOWN]
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_release_notes(changelog_content: str) -> list[tuple[str, list[str]]]:
|
||||||
|
"""Return all [(version, bullets)] entries sorted oldest-first."""
|
||||||
|
notes = parse_changelog(changelog_content)
|
||||||
|
versions = sorted(notes.keys(), key=lambda v: _coerce_version(v) or ())
|
||||||
|
return [(version, notes[version]) for version in versions if notes[version]]
|
||||||
|
|
||||||
|
|
||||||
|
def read_local_changelog(cwd: Path) -> str:
|
||||||
|
"""Read CHANGELOG.md from the workspace root, returning '' if missing."""
|
||||||
|
path = cwd / 'CHANGELOG.md'
|
||||||
|
try:
|
||||||
|
return path.read_text(encoding='utf-8')
|
||||||
|
except (OSError, UnicodeDecodeError):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def check_for_release_notes(
|
||||||
|
current_version: str,
|
||||||
|
last_seen_version: str | None,
|
||||||
|
cwd: Path | None = None,
|
||||||
|
changelog_content: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Return {'hasReleaseNotes': bool, 'releaseNotes': [...]}.
|
||||||
|
|
||||||
|
If `changelog_content` is supplied it is used directly; otherwise the
|
||||||
|
workspace CHANGELOG.md is read. Mirrors the npm `checkForReleaseNotes`
|
||||||
|
return shape (without the network fetch — no cache update).
|
||||||
|
"""
|
||||||
|
content = (
|
||||||
|
changelog_content
|
||||||
|
if changelog_content is not None
|
||||||
|
else read_local_changelog(cwd or Path.cwd())
|
||||||
|
)
|
||||||
|
bullets = get_recent_release_notes(current_version, last_seen_version, content)
|
||||||
|
return {
|
||||||
|
'hasReleaseNotes': bool(bullets),
|
||||||
|
'releaseNotes': bullets,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'MAX_RELEASE_NOTES_SHOWN',
|
||||||
|
'parse_changelog',
|
||||||
|
'get_recent_release_notes',
|
||||||
|
'get_all_release_notes',
|
||||||
|
'read_local_changelog',
|
||||||
|
'check_for_release_notes',
|
||||||
|
]
|
||||||
+77
-2
@@ -2,11 +2,61 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .deferred_init import DeferredInitResult, run_deferred_init
|
from .deferred_init import DeferredInitResult, run_deferred_init
|
||||||
from .prefetch import PrefetchResult, start_keychain_prefetch, start_mdm_raw_read, start_project_scan
|
from .prefetch import PrefetchResult, start_keychain_prefetch, start_mdm_raw_read, start_project_scan
|
||||||
|
from .release_notes import check_for_release_notes
|
||||||
|
|
||||||
|
|
||||||
|
MIN_PYTHON_VERSION: tuple[int, int] = (3, 10)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RuntimeRequirementCheck:
|
||||||
|
name: str
|
||||||
|
ok: bool
|
||||||
|
detail: str
|
||||||
|
|
||||||
|
|
||||||
|
def check_runtime_requirements() -> tuple[RuntimeRequirementCheck, ...]:
|
||||||
|
checks: list[RuntimeRequirementCheck] = []
|
||||||
|
py_major, py_minor = sys.version_info[:2]
|
||||||
|
py_ok = (py_major, py_minor) >= MIN_PYTHON_VERSION
|
||||||
|
checks.append(RuntimeRequirementCheck(
|
||||||
|
name='python_version',
|
||||||
|
ok=py_ok,
|
||||||
|
detail=(
|
||||||
|
f'Python {py_major}.{py_minor} >= {MIN_PYTHON_VERSION[0]}.{MIN_PYTHON_VERSION[1]}'
|
||||||
|
if py_ok
|
||||||
|
else f'Python {py_major}.{py_minor} is below required '
|
||||||
|
f'{MIN_PYTHON_VERSION[0]}.{MIN_PYTHON_VERSION[1]}'
|
||||||
|
),
|
||||||
|
))
|
||||||
|
impl = platform.python_implementation()
|
||||||
|
checks.append(RuntimeRequirementCheck(
|
||||||
|
name='python_implementation',
|
||||||
|
ok=True,
|
||||||
|
detail=impl,
|
||||||
|
))
|
||||||
|
machine = platform.machine() or 'unknown'
|
||||||
|
system = platform.system() or 'unknown'
|
||||||
|
checks.append(RuntimeRequirementCheck(
|
||||||
|
name='platform',
|
||||||
|
ok=True,
|
||||||
|
detail=f'{system} on {machine}',
|
||||||
|
))
|
||||||
|
return tuple(checks)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_package_version() -> str:
|
||||||
|
try:
|
||||||
|
from importlib.metadata import version
|
||||||
|
|
||||||
|
return version('claw-code-agent')
|
||||||
|
except Exception:
|
||||||
|
return '0.0.0'
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -34,6 +84,11 @@ class SetupReport:
|
|||||||
deferred_init: DeferredInitResult
|
deferred_init: DeferredInitResult
|
||||||
trusted: bool
|
trusted: bool
|
||||||
cwd: Path
|
cwd: Path
|
||||||
|
runtime_checks: tuple[RuntimeRequirementCheck, ...] = field(default_factory=tuple)
|
||||||
|
release_notes: tuple[str, ...] = field(default_factory=tuple)
|
||||||
|
|
||||||
|
def has_blocking_issues(self) -> bool:
|
||||||
|
return any(not check.ok for check in self.runtime_checks)
|
||||||
|
|
||||||
def as_markdown(self) -> str:
|
def as_markdown(self) -> str:
|
||||||
lines = [
|
lines = [
|
||||||
@@ -44,12 +99,21 @@ class SetupReport:
|
|||||||
f'- Trusted mode: {self.trusted}',
|
f'- Trusted mode: {self.trusted}',
|
||||||
f'- CWD: {self.cwd}',
|
f'- CWD: {self.cwd}',
|
||||||
'',
|
'',
|
||||||
|
'Runtime checks:',
|
||||||
|
*(
|
||||||
|
f'- {check.name}: {"ok" if check.ok else "FAIL"} — {check.detail}'
|
||||||
|
for check in self.runtime_checks
|
||||||
|
),
|
||||||
|
'',
|
||||||
'Prefetches:',
|
'Prefetches:',
|
||||||
*(f'- {prefetch.name}: {prefetch.detail}' for prefetch in self.prefetches),
|
*(f'- {prefetch.name}: {prefetch.detail}' for prefetch in self.prefetches),
|
||||||
'',
|
'',
|
||||||
'Deferred init:',
|
'Deferred init:',
|
||||||
*self.deferred_init.as_lines(),
|
*self.deferred_init.as_lines(),
|
||||||
]
|
]
|
||||||
|
if self.release_notes:
|
||||||
|
lines.extend(['', 'Release notes (newer than last seen):'])
|
||||||
|
lines.extend(f'- {note}' for note in self.release_notes)
|
||||||
return '\n'.join(lines)
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
@@ -61,17 +125,28 @@ def build_workspace_setup() -> WorkspaceSetup:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_setup(cwd: Path | None = None, trusted: bool = True) -> SetupReport:
|
def run_setup(
|
||||||
|
cwd: Path | None = None,
|
||||||
|
trusted: bool = True,
|
||||||
|
last_seen_version: str | None = None,
|
||||||
|
) -> SetupReport:
|
||||||
root = cwd or Path(__file__).resolve().parent.parent
|
root = cwd or Path(__file__).resolve().parent.parent
|
||||||
prefetches = [
|
prefetches = [
|
||||||
start_mdm_raw_read(),
|
start_mdm_raw_read(),
|
||||||
start_keychain_prefetch(),
|
start_keychain_prefetch(),
|
||||||
start_project_scan(root),
|
start_project_scan(root),
|
||||||
]
|
]
|
||||||
|
release_notes_payload = check_for_release_notes(
|
||||||
|
current_version=_read_package_version(),
|
||||||
|
last_seen_version=last_seen_version,
|
||||||
|
cwd=root,
|
||||||
|
)
|
||||||
return SetupReport(
|
return SetupReport(
|
||||||
setup=build_workspace_setup(),
|
setup=build_workspace_setup(),
|
||||||
prefetches=tuple(prefetches),
|
prefetches=tuple(prefetches),
|
||||||
deferred_init=run_deferred_init(trusted=trusted),
|
deferred_init=run_deferred_init(trusted=trusted),
|
||||||
trusted=trusted,
|
trusted=trusted,
|
||||||
cwd=root,
|
cwd=root,
|
||||||
|
runtime_checks=check_runtime_requirements(),
|
||||||
|
release_notes=tuple(release_notes_payload['releaseNotes']),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"""Tests for the local release-notes parser ported from utils/releaseNotes.ts."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from src.release_notes import (
|
||||||
|
MAX_RELEASE_NOTES_SHOWN,
|
||||||
|
check_for_release_notes,
|
||||||
|
get_all_release_notes,
|
||||||
|
get_recent_release_notes,
|
||||||
|
parse_changelog,
|
||||||
|
read_local_changelog,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
SAMPLE = (
|
||||||
|
'# Changelog\n\n'
|
||||||
|
'## 1.3.0 - 2026-04-15\n'
|
||||||
|
'- new shiny\n'
|
||||||
|
'- another bullet\n\n'
|
||||||
|
'## 1.2.0\n'
|
||||||
|
'- mid bullet\n\n'
|
||||||
|
'## 1.1.0\n'
|
||||||
|
'- oldest bullet\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ParseChangelogTest(unittest.TestCase):
|
||||||
|
def test_returns_empty_for_blank(self) -> None:
|
||||||
|
self.assertEqual(parse_changelog(''), {})
|
||||||
|
|
||||||
|
def test_extracts_versions_and_bullets(self) -> None:
|
||||||
|
parsed = parse_changelog(SAMPLE)
|
||||||
|
self.assertEqual(set(parsed.keys()), {'1.3.0', '1.2.0', '1.1.0'})
|
||||||
|
self.assertEqual(parsed['1.3.0'], ['new shiny', 'another bullet'])
|
||||||
|
self.assertEqual(parsed['1.2.0'], ['mid bullet'])
|
||||||
|
|
||||||
|
def test_skips_versions_without_bullets(self) -> None:
|
||||||
|
parsed = parse_changelog('# X\n\n## 1.0.0\nplain text\n')
|
||||||
|
self.assertEqual(parsed, {})
|
||||||
|
|
||||||
|
|
||||||
|
class RecentNotesTest(unittest.TestCase):
|
||||||
|
def test_returns_only_newer_versions(self) -> None:
|
||||||
|
notes = get_recent_release_notes('1.3.0', '1.2.0', SAMPLE)
|
||||||
|
self.assertEqual(notes, ['new shiny', 'another bullet'])
|
||||||
|
|
||||||
|
def test_first_run_returns_all(self) -> None:
|
||||||
|
notes = get_recent_release_notes('1.3.0', None, SAMPLE)
|
||||||
|
self.assertEqual(notes[0], 'new shiny')
|
||||||
|
self.assertIn('oldest bullet', notes)
|
||||||
|
|
||||||
|
def test_no_new_when_at_or_below_previous(self) -> None:
|
||||||
|
self.assertEqual(get_recent_release_notes('1.1.0', '1.3.0', SAMPLE), [])
|
||||||
|
|
||||||
|
def test_caps_at_max_shown(self) -> None:
|
||||||
|
big_changelog = '# Changelog\n\n' + ''.join(
|
||||||
|
f'## 9.9.{i}\n- bullet {i}\n\n' for i in range(20)
|
||||||
|
)
|
||||||
|
notes = get_recent_release_notes('9.9.19', '0.0.1', big_changelog)
|
||||||
|
self.assertEqual(len(notes), MAX_RELEASE_NOTES_SHOWN)
|
||||||
|
|
||||||
|
|
||||||
|
class AllNotesTest(unittest.TestCase):
|
||||||
|
def test_sorted_oldest_first(self) -> None:
|
||||||
|
all_notes = get_all_release_notes(SAMPLE)
|
||||||
|
versions = [version for version, _ in all_notes]
|
||||||
|
self.assertEqual(versions, ['1.1.0', '1.2.0', '1.3.0'])
|
||||||
|
|
||||||
|
|
||||||
|
class ReadLocalChangelogTest(unittest.TestCase):
|
||||||
|
def test_reads_when_present(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
(Path(tmp) / 'CHANGELOG.md').write_text(SAMPLE, encoding='utf-8')
|
||||||
|
self.assertIn('## 1.3.0', read_local_changelog(Path(tmp)))
|
||||||
|
|
||||||
|
def test_returns_empty_when_missing(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
self.assertEqual(read_local_changelog(Path(tmp)), '')
|
||||||
|
|
||||||
|
|
||||||
|
class CheckForReleaseNotesTest(unittest.TestCase):
|
||||||
|
def test_signals_when_changelog_present_and_newer(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
(Path(tmp) / 'CHANGELOG.md').write_text(SAMPLE, encoding='utf-8')
|
||||||
|
payload = check_for_release_notes('1.3.0', '1.2.0', cwd=Path(tmp))
|
||||||
|
self.assertTrue(payload['hasReleaseNotes'])
|
||||||
|
self.assertEqual(payload['releaseNotes'][0], 'new shiny')
|
||||||
|
|
||||||
|
def test_silent_when_missing_changelog(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
payload = check_for_release_notes('1.3.0', None, cwd=Path(tmp))
|
||||||
|
self.assertFalse(payload['hasReleaseNotes'])
|
||||||
|
self.assertEqual(payload['releaseNotes'], [])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""Tests for setup-time runtime checks ported from setup.ts."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from src.setup import (
|
||||||
|
MIN_PYTHON_VERSION,
|
||||||
|
SetupReport,
|
||||||
|
check_runtime_requirements,
|
||||||
|
run_setup,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RuntimeRequirementCheckTest(unittest.TestCase):
|
||||||
|
def test_python_check_passes_on_current_runtime(self) -> None:
|
||||||
|
checks = {check.name: check for check in check_runtime_requirements()}
|
||||||
|
self.assertTrue(checks['python_version'].ok)
|
||||||
|
self.assertGreaterEqual(sys.version_info[:2], MIN_PYTHON_VERSION)
|
||||||
|
|
||||||
|
def test_python_check_fails_when_below_minimum(self) -> None:
|
||||||
|
# Force a lower version_info via patching to verify the failure branch.
|
||||||
|
fake_version = mock.Mock()
|
||||||
|
fake_version.__getitem__ = lambda self, idx: (3, 8)[idx] if isinstance(idx, int) else (3, 8)[idx]
|
||||||
|
with mock.patch('src.setup.sys') as fake_sys:
|
||||||
|
fake_sys.version_info = (3, 8, 0)
|
||||||
|
checks = {check.name: check for check in check_runtime_requirements()}
|
||||||
|
self.assertFalse(checks['python_version'].ok)
|
||||||
|
self.assertIn('below required', checks['python_version'].detail)
|
||||||
|
|
||||||
|
def test_includes_platform_and_implementation(self) -> None:
|
||||||
|
names = {check.name for check in check_runtime_requirements()}
|
||||||
|
self.assertIn('python_implementation', names)
|
||||||
|
self.assertIn('platform', names)
|
||||||
|
|
||||||
|
|
||||||
|
class SetupReportTest(unittest.TestCase):
|
||||||
|
def test_run_setup_reports_runtime_and_release_notes(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
(Path(tmp) / 'CHANGELOG.md').write_text(
|
||||||
|
'# Changelog\n\n## 9.9.9\n- big release\n', encoding='utf-8',
|
||||||
|
)
|
||||||
|
report = run_setup(cwd=Path(tmp), trusted=True, last_seen_version='0.0.1')
|
||||||
|
self.assertIsInstance(report, SetupReport)
|
||||||
|
self.assertGreaterEqual(len(report.runtime_checks), 3)
|
||||||
|
self.assertIn('big release', report.release_notes)
|
||||||
|
markdown = report.as_markdown()
|
||||||
|
self.assertIn('Runtime checks', markdown)
|
||||||
|
self.assertIn('Release notes', markdown)
|
||||||
|
self.assertFalse(report.has_blocking_issues())
|
||||||
|
|
||||||
|
def test_no_release_notes_when_changelog_missing(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
report = run_setup(cwd=Path(tmp), trusted=True)
|
||||||
|
self.assertEqual(report.release_notes, ())
|
||||||
|
self.assertNotIn('Release notes', report.as_markdown())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user