feat: v0.6.0 — collapse policy, session limiter, Claude Code provider, prune, browser auth, media downloads
This commit is contained in:
+103
-2
@@ -124,10 +124,30 @@ class TestExportSinceValidation:
|
||||
"CHATGPT_SESSION_TOKEN": "eyJtesttoken",
|
||||
"CACHE_DIR": str(tmp_path),
|
||||
"EXPORT_DIR": str(tmp_path / "exports"),
|
||||
# This test hits the real (failing) auth endpoint with retries;
|
||||
# don't add politeness pacing on top of the backoff sleeps.
|
||||
"REQUEST_DELAY": "0",
|
||||
},
|
||||
)
|
||||
assert "Invalid --since date" not in result.output
|
||||
|
||||
def test_max_conversations_zero_rejected(self, tmp_path):
|
||||
"""--max-conversations uses IntRange(min=1); 0 must be rejected by click."""
|
||||
self._pre_populated_cache(tmp_path)
|
||||
|
||||
runner = CliRunner(mix_stderr=True)
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
["--no-log-file", "export", "--max-conversations", "0"],
|
||||
env={
|
||||
"CHATGPT_SESSION_TOKEN": "eyJtesttoken",
|
||||
"CACHE_DIR": str(tmp_path),
|
||||
"EXPORT_DIR": str(tmp_path / "exports"),
|
||||
},
|
||||
)
|
||||
assert result.exit_code == 2
|
||||
assert "max-conversations" in result.output
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# LossReport summary
|
||||
@@ -145,8 +165,9 @@ class TestLossReportSummary:
|
||||
assert "[export] Run summary:" in out
|
||||
assert "conversations: 0" in out
|
||||
assert "messages rendered: 0" in out
|
||||
# Both "(none)" sentinels present — never empty parens
|
||||
assert out.count("(none)") == 2
|
||||
# All three "(none)" sentinels present — never empty parens
|
||||
# (unknown blocks, extraction failures, collapsed by policy)
|
||||
assert out.count("(none)") == 3
|
||||
|
||||
def test_top_5_breakdown(self):
|
||||
from src.loss_report import LossReport
|
||||
@@ -174,3 +195,83 @@ class TestLossReportSummary:
|
||||
out = report.format_summary()
|
||||
assert "conversations: 1" in out
|
||||
assert "messages rendered: 2" in out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# prune command
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestPrune:
|
||||
"""prune deletes export files not referenced by the manifest."""
|
||||
|
||||
def _setup(self, tmp_path):
|
||||
"""Cache with one referenced file; one stale file + empty-dir candidate."""
|
||||
cache = Cache(tmp_path / "cache")
|
||||
cache.acknowledge_tos()
|
||||
export_dir = tmp_path / "exports"
|
||||
keep = export_dir / "chatgpt" / "proj.2026" / "keep.md"
|
||||
keep.parent.mkdir(parents=True)
|
||||
keep.write_text("kept")
|
||||
stale = export_dir / "chatgpt" / "proj" / "2026" / "old-layout.md"
|
||||
stale.parent.mkdir(parents=True)
|
||||
stale.write_text("stale")
|
||||
cache.mark_exported("chatgpt", "conv-1", {"file_path": str(keep)})
|
||||
return export_dir, keep, stale
|
||||
|
||||
def _invoke(self, tmp_path, *args):
|
||||
runner = CliRunner(mix_stderr=True)
|
||||
return runner.invoke(
|
||||
cli,
|
||||
["--no-log-file", "prune", *args],
|
||||
env={
|
||||
"CACHE_DIR": str(tmp_path / "cache"),
|
||||
"EXPORT_DIR": str(tmp_path / "exports"),
|
||||
},
|
||||
)
|
||||
|
||||
def test_dry_run_lists_but_keeps_files(self, tmp_path):
|
||||
export_dir, keep, stale = self._setup(tmp_path)
|
||||
result = self._invoke(tmp_path, "--dry-run")
|
||||
assert result.exit_code == 0
|
||||
assert "old-layout.md" in result.output
|
||||
assert "Dry run" in result.output
|
||||
assert stale.exists() and keep.exists()
|
||||
|
||||
def test_yes_deletes_stale_keeps_referenced_sweeps_dirs(self, tmp_path):
|
||||
export_dir, keep, stale = self._setup(tmp_path)
|
||||
result = self._invoke(tmp_path, "--yes")
|
||||
assert result.exit_code == 0
|
||||
assert not stale.exists()
|
||||
assert keep.exists()
|
||||
# Old-layout dirs are now empty and swept
|
||||
assert not (export_dir / "chatgpt" / "proj").exists()
|
||||
|
||||
def test_refuses_with_empty_manifest(self, tmp_path):
|
||||
"""Footgun guard: after cache --clear, prune must not wipe the archive."""
|
||||
cache = Cache(tmp_path / "cache")
|
||||
cache.acknowledge_tos()
|
||||
export_dir = tmp_path / "exports"
|
||||
f = export_dir / "chatgpt" / "a.md"
|
||||
f.parent.mkdir(parents=True)
|
||||
f.write_text("data")
|
||||
result = self._invoke(tmp_path, "--yes")
|
||||
assert result.exit_code == 1
|
||||
assert "Refusing to prune" in result.output
|
||||
assert f.exists()
|
||||
|
||||
def test_aborts_without_confirmation(self, tmp_path):
|
||||
export_dir, keep, stale = self._setup(tmp_path)
|
||||
runner = CliRunner(mix_stderr=True)
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
["--no-log-file", "prune"],
|
||||
input="n\n",
|
||||
env={
|
||||
"CACHE_DIR": str(tmp_path / "cache"),
|
||||
"EXPORT_DIR": str(tmp_path / "exports"),
|
||||
},
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "Aborted" in result.output
|
||||
assert stale.exists()
|
||||
|
||||
Reference in New Issue
Block a user