Skip to content

Commit fa8564b

Browse files
fix(cli): accurate attempt count, elif, stdout on success in exit summary
- m = n + k (exported + failed) instead of total_candidates; avoids misleading counts for incremental no-ops and --since last date-filtered sessions - Use elif so mutual exclusion of exit-1/exit-2 is explicit - Route summary to stdout when k==0 (clean success), stderr when k>0; prevents cron monitors treating clean exports as failures - Add test_since_last_early_return_exits_one_on_failure: validates actual exit code 1 without mocking _exit_bulk_export - Use explicit base/test-project instead of next(base.iterdir()) - Add comment on sys.exit branches and --session doc note in docstring
1 parent 719fb9a commit fa8564b

2 files changed

Lines changed: 44 additions & 8 deletions

File tree

scripts/export.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
0 — all sessions exported successfully (or nothing to export, no errors)
1616
1 — total failure (no sessions exported; one or more errors)
1717
2 — partial failure (some sessions exported, some failed)
18+
(exit codes apply to bulk export only; --session single-export always exits 0)
1819
"""
1920

2021
import argparse
@@ -400,15 +401,21 @@ def _aggregate_stats(base_dir: str, project_filter: str, fmt: str):
400401

401402

402403
def _exit_bulk_export(result: BulkExportResult) -> None:
403-
"""Map bulk-export counts to process exit code (CLI wrapper only)."""
404+
"""Map bulk-export counts to process exit code (CLI wrapper only).
405+
406+
Prints a summary to stderr on any failure, stdout on clean success.
407+
Raises SystemExit(1) for total failure, SystemExit(2) for partial.
408+
"""
404409
n = result.exported_session_count
405-
m = result.total_candidates
406410
k = result.failure_count
411+
# "attempted" = exported + failed; excludes untitled/excluded/mtime-skipped
412+
m = n + k
407413
if n > 0 or k > 0:
408-
print(f"Exported {n} of {m} sessions ({k} failed)", file=sys.stderr)
409-
if n == 0 and k > 0:
414+
dest = sys.stderr if k > 0 else sys.stdout
415+
print(f"Exported {n} of {m} sessions ({k} failed)", file=dest)
416+
if n == 0 and k > 0: # total failure
410417
sys.exit(1)
411-
if k > 0:
418+
elif k > 0: # partial failure
412419
sys.exit(2)
413420

414421

tests/test_cli_export_exit_codes.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,17 @@ def test_cli_export_clean_exits_zero(tmp_path):
5757
])
5858
assert proc.returncode == 0, proc.stderr
5959
assert list(out_dir.rglob("*.md"))
60-
if proc.stderr.strip():
61-
assert "failed" not in proc.stderr.lower() or "0 failed" in proc.stderr
60+
# Success summary must go to stdout, not stderr
61+
assert "Exported" not in proc.stderr
62+
assert "Exported 1 of 1 sessions (0 failed)" in proc.stdout
6263

6364

6465
def test_cli_export_partial_failure_exits_two(
6566
tmp_path, monkeypatch, capsys
6667
):
6768
"""One session exports; a second fails parse (simulated corrupt file)."""
6869
base = _seed_base_dir(tmp_path)
69-
project_dir = next(base.iterdir())
70+
project_dir = base / "test-project"
7071
bad = project_dir / "session_bad.jsonl"
7172
bad.write_text('{"type": "user"}\n', encoding="utf-8")
7273
out_dir = tmp_path / "out"
@@ -134,6 +135,34 @@ def _track_exit(result: BulkExportResult) -> None:
134135
assert "Exported" not in captured.err
135136

136137

138+
def test_since_last_early_return_exits_one_on_failure(
139+
tmp_path, monkeypatch, capsys
140+
):
141+
"""Since-last early-return with failure_count>0 must produce real exit code 1."""
142+
fake_result = BulkExportResult(latest_day=None, failure_count=1)
143+
144+
monkeypatch.setattr(export, "run_bulk_export", lambda **kwargs: fake_result)
145+
monkeypatch.setattr(export, "list_projects", lambda base: [{"name": "p", "path": "/p"}])
146+
147+
args = types.SimpleNamespace(
148+
base_dir=str(tmp_path),
149+
out=str(tmp_path / "out"),
150+
since="last",
151+
no_zip=True,
152+
project=None,
153+
format="md",
154+
session=None,
155+
exclude_rules=None,
156+
)
157+
158+
with pytest.raises(SystemExit) as exc_info:
159+
export.cmd_export(args)
160+
161+
assert exc_info.value.code == 1
162+
captured = capsys.readouterr()
163+
assert "Exported 0 of 1 sessions (1 failed)" in captured.err
164+
165+
137166
def test_cli_export_incremental_noop_no_stderr_summary(tmp_path):
138167
"""Second incremental run after state is saved: exit 0, no stderr summary."""
139168
base = _seed_base_dir(tmp_path)

0 commit comments

Comments
 (0)