Skip to content

Commit c8a3f62

Browse files
Lukas Geigerclaude
andcommitted
feat: add cross-platform smoke CI + bump version to 1.1.3
- tests/source_platform_smoke.py: 9 platform-neutral tests (rrule, config, TOML, catchup) that run on macOS and Linux without any Windows-specific code - .github/workflows/source-platform-smoke.yml: matrix CI on ubuntu-latest + macos-latest, Python 3.11 + 3.12 - PORTIERUNGSPLAN.md: documents Windows-first policy and platform boundary - CHANGELOG.md: promote [Unreleased] tray fix into [1.1.3] - 2026-06-10 - pyproject.toml: bump version 1.1.2 → 1.1.3 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b91fcc2 commit c8a3f62

5 files changed

Lines changed: 240 additions & 1 deletion

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Source Platform Smoke
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
smoke:
9+
strategy:
10+
matrix:
11+
os: [ubuntu-latest, macos-latest]
12+
python-version: ["3.11", "3.12"]
13+
runs-on: ${{ matrix.os }}
14+
steps:
15+
- uses: actions/checkout@v4
16+
- uses: actions/setup-python@v5
17+
with:
18+
python-version: ${{ matrix.python-version }}
19+
- name: Install
20+
run: python -m pip install -e ".[dev]"
21+
- name: Smoke tests
22+
run: pytest tests/source_platform_smoke.py -v

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to this project are documented here.
44

55
## [Unreleased]
66

7+
## [1.1.3] - 2026-06-10
8+
9+
### Added
10+
11+
- Cross-platform smoke tests (`tests/source_platform_smoke.py`) for rrule, config, TOML, and catchup logic on macOS and Linux.
12+
- GitHub Actions workflow `source-platform-smoke.yml` running on ubuntu-latest and macos-latest.
13+
- `PORTIERUNGSPLAN.md` documenting the Windows-first platform boundary.
14+
715
### Fixed
816

917
- Fixed direct execution of `tray_app.py` so the windowed EXE entrypoint can import the package CLI without a parent package context.

PORTIERUNGSPLAN.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# PORTIERUNGSPLAN — safe-start-for-codex
2+
3+
## Plattform-Strategie
4+
5+
**Windows-first:** safe-start-for-codex ist ein Windows-Startup-Gate für Codex Desktop.
6+
Kernfunktionen (Prozess-Launch, Store-AUMID, Aufräumen von Start-Blockern) setzen
7+
Windows-APIs voraus und sind explizit mit `if os.name != "nt"` abgesichert.
8+
9+
**macOS/Linux als Smoke-Ziele:** Die plattformneutralen Schichten (rrule-Parser, TOML-Scanner,
10+
config.json-Verwaltung, Catchup-Planung) werden auf macOS und Linux kontinuierlich getestet
11+
(`tests/source_platform_smoke.py`, Workflow `source-platform-smoke.yml`). Das sichert, dass
12+
Imports, Datenmodelle und Berechnungen plattformübergreifend korrekt bleiben.
13+
14+
## Grenzen
15+
16+
| Schicht | Windows | macOS | Linux |
17+
|---------|---------|-------|-------|
18+
| rrule-Parser ||||
19+
| TOML-Scanner ||||
20+
| Config read/write ||||
21+
| Catchup-Bericht ||||
22+
| Tray-App (pystray) || Nur mit Display | Nur mit Display |
23+
| Prozess-Start (Codex.exe) ||||
24+
| Store-Start (AUMID) ||||
25+
| `cleanup_start_blockers` || No-op | No-op |
26+
| `SafeStartGate.run()` || No-op | No-op |
27+
28+
## Tray auf Nicht-Windows
29+
30+
pystray funktioniert unter macOS (mit AppKit) und Linux (mit GTK/AppIndicator), benötigt
31+
aber eine Display-Umgebung. Headless-CI führt keine Tray-Tests aus.
32+
33+
## Nicht geplant
34+
35+
- macOS/Linux Store-Integration
36+
- Mobile, Browser, Flutter-Ports

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "safe-start-for-codex"
7-
version = "1.1.2"
7+
version = "1.1.3"
88
description = "Unofficial Windows startup gate for Codex Desktop automations."
99
readme = "README.md"
1010
requires-python = ">=3.11"

tests/source_platform_smoke.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"""Cross-platform smoke tests — run on Windows, macOS, and Linux.
2+
3+
These tests exercise only platform-neutral code paths in cli.py.
4+
Windows-specific functions (cleanup_start_blockers, SafeStartGate.run, etc.)
5+
are NOT tested here; they are covered by tests/test_cli.py on windows-latest.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import json
11+
from datetime import datetime
12+
from pathlib import Path
13+
14+
import pytest
15+
16+
from safe_start_for_codex.cli import (
17+
build_catchup_report,
18+
command_config_init,
19+
default_config_path,
20+
load_automations,
21+
read_gate_config,
22+
rrule_effective_period_hours,
23+
rrule_next_after,
24+
rrule_occurrences_between,
25+
set_status,
26+
)
27+
28+
29+
# ---------------------------------------------------------------------------
30+
# Helpers
31+
# ---------------------------------------------------------------------------
32+
33+
def write_automation(root: Path, name: str, status: str, rrule: str) -> Path:
34+
directory = root / "automations" / name
35+
directory.mkdir(parents=True)
36+
path = directory / "automation.toml"
37+
path.write_text(
38+
"\n".join(
39+
[
40+
f'id = "{name}"',
41+
f'name = "{name}"',
42+
'kind = "cron"',
43+
f'rrule = "{rrule}"',
44+
f'status = "{status}"',
45+
"created_at = 1000",
46+
"updated_at = 1000",
47+
"",
48+
]
49+
),
50+
encoding="utf-8",
51+
)
52+
return path
53+
54+
55+
# ---------------------------------------------------------------------------
56+
# 1. Package importable
57+
# ---------------------------------------------------------------------------
58+
59+
def test_package_imports() -> None:
60+
"""The package must import without errors on all platforms."""
61+
import safe_start_for_codex.cli as m
62+
assert hasattr(m, "SafeStartGate")
63+
assert hasattr(m, "load_automations")
64+
65+
66+
# ---------------------------------------------------------------------------
67+
# 2. CODEX_HOME isolation via env var
68+
# ---------------------------------------------------------------------------
69+
70+
def test_codex_home_env_isolation(tmp_path: Path, monkeypatch) -> None:
71+
"""CODEX_HOME env var must redirect all path resolution."""
72+
monkeypatch.setenv("CODEX_HOME", str(tmp_path))
73+
write_automation(tmp_path, "smoke-a", "ACTIVE", "RRULE:FREQ=DAILY;BYHOUR=8;BYMINUTE=0")
74+
75+
items = load_automations()
76+
77+
assert len(items) == 1
78+
assert items[0].id == "smoke-a"
79+
assert items[0].status == "ACTIVE"
80+
81+
82+
# ---------------------------------------------------------------------------
83+
# 3. rrule parsing — platform-neutral
84+
# ---------------------------------------------------------------------------
85+
86+
def test_rrule_next_after_daily() -> None:
87+
rule = "RRULE:FREQ=DAILY;BYHOUR=9;BYMINUTE=0"
88+
anchor = datetime(2026, 1, 1, 8, 0)
89+
nxt = rrule_next_after(rule, anchor)
90+
assert nxt is not None
91+
assert nxt.hour == 9
92+
assert nxt > anchor
93+
94+
95+
def test_rrule_occurrences_between_counts() -> None:
96+
rule = "RRULE:FREQ=DAILY;BYHOUR=9;BYMINUTE=0"
97+
start = datetime(2026, 1, 1, 0, 0)
98+
end = datetime(2026, 1, 8, 0, 0)
99+
occurrences = rrule_occurrences_between(rule, start, end)
100+
assert len(occurrences) == 7
101+
102+
103+
def test_rrule_effective_period_daily() -> None:
104+
rule = "RRULE:FREQ=DAILY;BYHOUR=9;BYMINUTE=0"
105+
hours = rrule_effective_period_hours(rule)
106+
assert hours is not None
107+
assert abs(hours - 24.0) < 0.1
108+
109+
110+
def test_rrule_effective_period_hourly() -> None:
111+
rule = "RRULE:FREQ=HOURLY;INTERVAL=4"
112+
hours = rrule_effective_period_hours(rule)
113+
assert hours is not None
114+
assert abs(hours - 4.0) < 0.1
115+
116+
117+
# ---------------------------------------------------------------------------
118+
# 4. Config read/write roundtrip
119+
# ---------------------------------------------------------------------------
120+
121+
def test_config_init_and_read(tmp_path: Path, monkeypatch) -> None:
122+
"""config-init writes a valid JSON file; read_gate_config reads it back."""
123+
monkeypatch.setenv("CODEX_HOME", str(tmp_path))
124+
config_path = default_config_path()
125+
126+
import argparse
127+
args = argparse.Namespace(config=None, force=False)
128+
command_config_init(args)
129+
130+
assert config_path.exists()
131+
data = json.loads(config_path.read_text(encoding="utf-8"))
132+
assert data["initial_release"] == 3
133+
assert data["interval_minutes"] == 5
134+
135+
settings, _, _ = read_gate_config(config_path)
136+
assert settings.initial_release == 3
137+
assert settings.interval_minutes == 5
138+
139+
140+
# ---------------------------------------------------------------------------
141+
# 5. set_status TOML edit (no Windows dep)
142+
# ---------------------------------------------------------------------------
143+
144+
def test_set_status_changes_toml(tmp_path: Path, monkeypatch) -> None:
145+
monkeypatch.setenv("CODEX_HOME", str(tmp_path))
146+
toml_path = write_automation(tmp_path, "edit-me", "ACTIVE", "RRULE:FREQ=DAILY")
147+
148+
result = set_status(toml_path, "PAUSED")
149+
150+
assert result is True
151+
content = toml_path.read_text(encoding="utf-8")
152+
assert 'status = "PAUSED"' in content
153+
assert 'status = "ACTIVE"' not in content
154+
155+
156+
# ---------------------------------------------------------------------------
157+
# 6. build_catchup_report (no SQLite/Windows dep)
158+
# ---------------------------------------------------------------------------
159+
160+
def test_build_catchup_report_no_observed(tmp_path: Path, monkeypatch) -> None:
161+
"""With no observed runs catchup report should have no candidates."""
162+
monkeypatch.setenv("CODEX_HOME", str(tmp_path))
163+
write_automation(tmp_path, "rare-weekly", "ACTIVE", "RRULE:FREQ=WEEKLY;BYHOUR=10;BYMINUTE=0")
164+
165+
automations = load_automations()
166+
report = build_catchup_report(
167+
automations=automations,
168+
observed_runs={},
169+
now=datetime(2026, 6, 10, 12, 0),
170+
)
171+
172+
assert report is not None
173+
assert hasattr(report, "candidates")

0 commit comments

Comments
 (0)