elliot-stack 1.0.39 → 1.0.41

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 (55) hide show
  1. package/package.json +1 -1
  2. package/skills/estack-migrate-claude-session-history/SKILL.md +3 -2
  3. package/skills/estack-migrate-claude-session-history/scripts/__pycache__/validate-migration.cpython-313.pyc +0 -0
  4. package/skills/estack-read-claude-session-history/SKILL.md +34 -3
  5. package/skills/estack-read-claude-session-history/references/modes.md +53 -7
  6. package/skills/estack-read-claude-session-history/references/recipes.md +7 -1
  7. package/skills/estack-read-claude-session-history/scripts/__pycache__/read_transcript.cpython-313.pyc +0 -0
  8. package/skills/estack-read-claude-session-history/scripts/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  9. package/skills/estack-read-claude-session-history/scripts/lib/__pycache__/parser.cpython-313.pyc +0 -0
  10. package/skills/estack-read-claude-session-history/scripts/lib/__pycache__/paths.cpython-313.pyc +0 -0
  11. package/skills/estack-read-claude-session-history/scripts/lib/__pycache__/search.cpython-313.pyc +0 -0
  12. package/skills/estack-read-claude-session-history/scripts/lib/__pycache__/subagents.cpython-313.pyc +0 -0
  13. package/skills/estack-read-claude-session-history/scripts/lib/__pycache__/tools.cpython-313.pyc +0 -0
  14. package/skills/estack-read-claude-session-history/scripts/read_transcript.py +163 -20
  15. package/skills/estack-migrate-claude-session-history/scripts/test-append-note.js +0 -48
  16. package/skills/estack-migrate-claude-session-history/scripts/test-validate-migration.py +0 -326
  17. package/skills/estack-read-claude-session-history/scripts/tests/conftest.py +0 -40
  18. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/README.md +0 -20
  19. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/all-noise.jsonl +0 -4
  20. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/basic-session.jsonl +0 -2
  21. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-gaps.jsonl +0 -9
  22. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-noise.jsonl +0 -7
  23. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-parallel-a.jsonl +0 -3
  24. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-parallel-b.jsonl +0 -3
  25. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-waiting.jsonl +0 -5
  26. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/interrupted.jsonl +0 -2
  27. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/multi-compact.jsonl +0 -8
  28. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/pending-user.jsonl +0 -2
  29. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-no-meta/subagents/agent-aaa.jsonl +0 -2
  30. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-no-meta.jsonl +0 -2
  31. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-parent/subagents/agent-xyz123.jsonl +0 -2
  32. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-parent/subagents/agent-xyz123.meta.json +0 -1
  33. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-parent.jsonl +0 -4
  34. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/time-spread.jsonl +0 -6
  35. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/timeline-day-test.jsonl +0 -5
  36. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/tool-usage-parent/subagents/agent-sub1.jsonl +0 -3
  37. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/tool-usage-parent.jsonl +0 -3
  38. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/tool-zoo.jsonl +0 -10
  39. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/truncated.jsonl +0 -3
  40. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/unicode.jsonl +0 -2
  41. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/with-advisor.jsonl +0 -3
  42. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/with-compact.jsonl +0 -5
  43. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/with-thinking.jsonl +0 -2
  44. package/skills/estack-read-claude-session-history/scripts/tests/test_backup_roots.py +0 -56
  45. package/skills/estack-read-claude-session-history/scripts/tests/test_engagement.py +0 -239
  46. package/skills/estack-read-claude-session-history/scripts/tests/test_json_format.py +0 -201
  47. package/skills/estack-read-claude-session-history/scripts/tests/test_modes.py +0 -323
  48. package/skills/estack-read-claude-session-history/scripts/tests/test_parser.py +0 -204
  49. package/skills/estack-read-claude-session-history/scripts/tests/test_paths.py +0 -133
  50. package/skills/estack-read-claude-session-history/scripts/tests/test_search.py +0 -94
  51. package/skills/estack-read-claude-session-history/scripts/tests/test_search_output.py +0 -161
  52. package/skills/estack-read-claude-session-history/scripts/tests/test_subagents.py +0 -43
  53. package/skills/estack-read-claude-session-history/scripts/tests/test_timeline.py +0 -179
  54. package/skills/estack-read-claude-session-history/scripts/tests/test_timezone_and_project.py +0 -225
  55. package/skills/estack-read-claude-session-history/scripts/tests/test_tools.py +0 -80
@@ -1,94 +0,0 @@
1
- """Tests for lib.search."""
2
-
3
- from datetime import datetime, timedelta
4
- from pathlib import Path
5
-
6
- from lib import parser as PR
7
- from lib import search as S
8
-
9
-
10
- def test_search_session_text(fixtures_dir):
11
- matches = S.search_session(
12
- fixtures_dir / "basic-session.jsonl", "Hello", role="both", in_channel="text"
13
- )
14
- assert len(matches) >= 1
15
-
16
-
17
- def test_search_session_assistant_only(fixtures_dir):
18
- matches = S.search_session(
19
- fixtures_dir / "basic-session.jsonl", "Hello",
20
- role="assistant", in_channel="text",
21
- )
22
- # "Hello" is in the user message only
23
- assert len(matches) == 0
24
-
25
-
26
- def test_search_session_user_only(fixtures_dir):
27
- matches = S.search_session(
28
- fixtures_dir / "basic-session.jsonl", "Hello",
29
- role="user", in_channel="text",
30
- )
31
- assert len(matches) >= 1
32
-
33
-
34
- def test_search_in_tool_use(fixtures_dir):
35
- matches = S.search_session(
36
- fixtures_dir / "tool-zoo.jsonl", "ls -la",
37
- role="both", in_channel="tool_use",
38
- )
39
- assert len(matches) >= 1
40
- assert matches[0].where == "tool_use"
41
-
42
-
43
- def test_search_in_thinking(fixtures_dir):
44
- matches = S.search_session(
45
- fixtures_dir / "with-thinking.jsonl", "step by step",
46
- role="both", in_channel="thinking",
47
- )
48
- assert len(matches) >= 1
49
- assert matches[0].where == "thinking"
50
-
51
-
52
- def test_search_no_match(fixtures_dir):
53
- matches = S.search_session(
54
- fixtures_dir / "basic-session.jsonl", "this-string-not-present",
55
- role="both", in_channel="text",
56
- )
57
- assert matches == []
58
-
59
-
60
- def test_search_with_time_filter(fixtures_dir):
61
- # "Hello" appears in basic-session.jsonl at 2026-05-01T10:00:00Z
62
- # Excluding that date should yield no matches
63
- matches = S.search_session(
64
- fixtures_dir / "basic-session.jsonl", "Hello",
65
- role="both", in_channel="text",
66
- since=datetime(2026, 6, 1),
67
- )
68
- assert matches == []
69
-
70
-
71
- def test_search_until_is_exclusive(fixtures_dir):
72
- # The "Hello" user message is stamped 2026-05-01T10:00:00Z. Derive the bound
73
- # from the parser (it converts to local naive time) so this is tz-independent.
74
- # Half-open [since, until): until at that instant excludes it; one second later includes it.
75
- at = PR._parse_timestamp("2026-05-01T10:00:00Z")
76
- assert S.search_session(
77
- fixtures_dir / "basic-session.jsonl", "Hello", role="both", until=at
78
- ) == []
79
- # "Hello" matches only the user message at the boundary instant, so +1s yields exactly 1.
80
- assert len(S.search_session(
81
- fixtures_dir / "basic-session.jsonl", "Hello", role="both",
82
- until=at + timedelta(seconds=1)
83
- )) == 1
84
-
85
-
86
- def test_search_project(fixtures_dir, tmp_path):
87
- # Copy 2 fixtures into a fake project dir and search
88
- import shutil
89
- pd = tmp_path / "fake-proj"
90
- pd.mkdir()
91
- shutil.copy(fixtures_dir / "basic-session.jsonl", pd / "session-a.jsonl")
92
- shutil.copy(fixtures_dir / "tool-zoo.jsonl", pd / "session-b.jsonl")
93
- matches = list(S.search_project(pd, "Hello", progress=False))
94
- assert len(matches) >= 1
@@ -1,161 +0,0 @@
1
- """Tests for cross-scope search output budgeting: summary-by-default, --full,
2
- the char-budget degrade, and TTY-gated progress."""
3
-
4
- import json
5
- import os
6
- import shutil
7
- import subprocess
8
- import sys
9
- from pathlib import Path
10
-
11
- import pytest
12
-
13
- import read_transcript as RT
14
- from lib.search import Match
15
-
16
-
17
- def _run_cli(cli_path, *args):
18
- env = dict(os.environ)
19
- env["PYTHONIOENCODING"] = "utf-8"
20
- return subprocess.run(
21
- [sys.executable, str(cli_path), *args],
22
- capture_output=True, text=True, encoding="utf-8", env=env,
23
- )
24
-
25
-
26
- # A realistic epoch base so _fmt_mtime renders sane dates if output is printed.
27
- _BASE_MTIME = 1_700_000_000
28
-
29
-
30
- def _mk(stem, project, mtime, window, role="assistant", where="text"):
31
- sp = Path("/root") / project / f"{stem}.jsonl"
32
- return Match(session_path=sp, mtime=mtime, role=role, where=where,
33
- timestamp=None, window_text=window)
34
-
35
-
36
- @pytest.fixture
37
- def wide_root(fixtures_dir, tmp_path):
38
- """A fake projects root with two projects, each holding a session."""
39
- root = tmp_path / "projects"
40
- keel = root / "C--Users-x-Keel-Project"
41
- other = root / "C--Users-x-Other-Claude-Code"
42
- keel.mkdir(parents=True)
43
- other.mkdir(parents=True)
44
- shutil.copy(fixtures_dir / "basic-session.jsonl", keel / "keelsession.jsonl")
45
- shutil.copy(fixtures_dir / "basic-session.jsonl", other / "othersession.jsonl")
46
- return root
47
-
48
-
49
- # ── unit: renderers ──────────────────────────────────────────────────────────
50
-
51
- def test_one_line_collapses_and_truncates():
52
- assert RT._one_line("a\n b c", 100) == "a b c"
53
- assert RT._one_line("x" * 200, 10) == "x" * 10 + "…"
54
- assert RT._one_line("", 10) == ""
55
-
56
-
57
- def test_summary_counts_all_hits_and_orders_newest_first():
58
- matches = [
59
- _mk("aaaaaaaa11", "C--Users-x-Proj", _BASE_MTIME, "hello world"),
60
- _mk("aaaaaaaa11", "C--Users-x-Proj", _BASE_MTIME, "hello again"),
61
- _mk("bbbbbbbb22", "C--Users-x-Proj", _BASE_MTIME + 100, "hello there"),
62
- ]
63
- out = RT._render_search_summary("hello", matches)
64
- assert '"hello": 3 matches across 2 sessions' in out
65
- assert "2 hits" in out # the aaaa session has 2
66
- # bbbb (BASE+100, newer) sorts before aaaa (BASE, older) — newest first
67
- assert out.index("bbbbbbbb") < out.index("aaaaaaaa")
68
- assert "--- Match #" not in out # no full windows in summary
69
-
70
-
71
- def test_summary_caps_sessions_but_counts_all(monkeypatch):
72
- monkeypatch.setattr(RT, "SEARCH_SUMMARY_SESSION_CAP", 2)
73
- matches = [_mk(f"sess{i:04d}xx", "C--Users-x-Proj", _BASE_MTIME + i, "hit") for i in range(5)]
74
- out = RT._render_search_summary("hit", matches)
75
- assert "5 matches across 5 sessions" in out # header counts everything
76
- assert "3 more session" in out # 5 total - 2 shown
77
-
78
-
79
- def test_search_summary_json_has_no_windows():
80
- matches = [_mk("aaaaaaaa11", "C--Users-x-My-Proj", _BASE_MTIME, "hello world snippet")]
81
- js = RT._search_summary_json(matches)
82
- assert js[0]["uuid"] == "aaaaaaaa11"
83
- assert js[0]["hits"] == 1
84
- assert "window" not in js[0]
85
- assert js[0]["first_snippet"] == "hello world snippet"
86
-
87
-
88
- def test_single_file_full_degrades_to_summary_when_oversized(monkeypatch, fixtures_dir):
89
- # Even a single-file search degrades if its full windows exceed the budget.
90
- monkeypatch.setattr(RT, "SEARCH_CHAR_BUDGET", 10)
91
- out = RT.mode_search_v2(
92
- root=Path("."), cwd=None, all_projects=False,
93
- file_path=fixtures_dir / "basic-session.jsonl",
94
- query="Hello", role="both", in_channel="text",
95
- since=None, until=None, fmt="text",
96
- )
97
- assert out.startswith("[note: full output")
98
- assert '"Hello":' in out # summary header is appended after the note
99
-
100
-
101
- def test_wide_full_degrades_to_summary_when_oversized(monkeypatch, wide_root):
102
- # The advertised path: wide scope + --full, oversized, degrades to summary.
103
- monkeypatch.setattr(RT, "SEARCH_CHAR_BUDGET", 10)
104
- out = RT.mode_search_v2(
105
- root=wide_root, cwd=None, all_projects=True, file_path=None,
106
- query="Hello", role="both", in_channel="text",
107
- since=None, until=None, fmt="text", full=True,
108
- )
109
- assert out.startswith("[note: full output")
110
- assert "matches across" in out # summary header appended
111
- assert "--- Match #" not in out # degraded — no windows survive
112
-
113
-
114
- # ── CLI: end-to-end ──────────────────────────────────────────────────────────
115
-
116
- def test_wide_search_summarizes_by_default(cli_path, wide_root):
117
- r = _run_cli(cli_path, "--root", str(wide_root), "--mode", "search",
118
- "--query", "Hello", "--all-projects")
119
- assert r.returncode == 0
120
- assert '"Hello":' in r.stdout and "matches across" in r.stdout # real summary header
121
- assert "--- Match #" not in r.stdout # summary, not windows
122
-
123
-
124
- def test_wide_search_full_shows_windows(cli_path, wide_root):
125
- r = _run_cli(cli_path, "--root", str(wide_root), "--mode", "search",
126
- "--query", "Hello", "--all-projects", "--full")
127
- assert r.returncode == 0
128
- assert "--- Match #" in r.stdout # full windows present
129
-
130
-
131
- def test_progress_suppressed_when_stderr_not_a_tty(cli_path, wide_root):
132
- # stderr is captured (not a TTY) — the Searching i/N progress must not appear.
133
- r = _run_cli(cli_path, "--root", str(wide_root), "--mode", "search",
134
- "--query", "Hello", "--all-projects")
135
- assert "Searching" not in r.stderr
136
-
137
-
138
- def test_cwd_search_matches_user_messages(cli_path, wide_root):
139
- # "Hello" lives only in the user message of basic-session.jsonl. A --cwd
140
- # search now routes through mode_search_v2 (role=both), so it must be found —
141
- # the old assistant-only --cwd path would have missed it.
142
- found = _run_cli(cli_path, "--root", str(wide_root), "--mode", "search",
143
- "--query", "Hello", "--cwd", "Keel")
144
- assert found.returncode == 0
145
- assert '"Hello":' in found.stdout and "across" in found.stdout
146
- assert "keelsess" in found.stdout
147
-
148
- # Restricting to assistant role excludes the user-only hit.
149
- asst = _run_cli(cli_path, "--root", str(wide_root), "--mode", "search",
150
- "--query", "Hello", "--cwd", "Keel", "--role", "assistant")
151
- assert asst.returncode == 0
152
- assert "No matches" in asst.stdout
153
-
154
-
155
- def test_json_role_filter_excludes_user_only_match(cli_path, wide_root):
156
- # JSON path honors --role too: assistant-only over a user-only term → empty list.
157
- r = _run_cli(cli_path, "--root", str(wide_root), "--mode", "search",
158
- "--query", "Hello", "--cwd", "Keel",
159
- "--role", "assistant", "--format", "json")
160
- assert r.returncode == 0
161
- assert json.loads(r.stdout) == []
@@ -1,43 +0,0 @@
1
- """Tests for lib.subagents."""
2
-
3
- from pathlib import Path
4
-
5
- from lib import subagents as SA
6
- from lib import paths as P
7
-
8
-
9
- def test_load_meta_hit(fixtures_dir):
10
- agent_file = fixtures_dir / "subagent-parent" / "subagents" / "agent-xyz123.jsonl"
11
- meta = SA.load_meta(agent_file)
12
- assert meta["agentType"] == "Explore"
13
- assert "bug" in meta["description"].lower()
14
-
15
-
16
- def test_load_meta_miss(fixtures_dir):
17
- agent_file = fixtures_dir / "subagent-no-meta" / "subagents" / "agent-aaa.jsonl"
18
- meta = SA.load_meta(agent_file)
19
- assert meta["agentType"] == "unknown"
20
- assert meta["description"] == ""
21
-
22
-
23
- def test_agent_finals(fixtures_dir):
24
- parent = fixtures_dir / "subagent-parent.jsonl"
25
- finals = SA.agent_finals(parent)
26
- assert len(finals) == 1
27
- agent_id, meta, text = finals[0]
28
- assert agent_id == "agent-xyz123"
29
- assert meta["agentType"] == "Explore"
30
- assert "Found it" in text
31
-
32
-
33
- def test_agent_finals_no_subagents(fixtures_dir):
34
- parent = fixtures_dir / "basic-session.jsonl"
35
- finals = SA.agent_finals(parent)
36
- assert finals == []
37
-
38
-
39
- def test_list_subagents(fixtures_dir):
40
- parent = fixtures_dir / "subagent-parent.jsonl"
41
- subs = P.list_subagents(parent)
42
- assert len(subs) == 1
43
- assert subs[0].stem == "agent-xyz123"
@@ -1,179 +0,0 @@
1
- """Tests for the timeline mode (build/render/gap parsing + CLI)."""
2
-
3
- import json
4
- import os
5
- import shutil
6
- import subprocess
7
- import sys
8
- from datetime import datetime, timedelta
9
- from pathlib import Path
10
-
11
- import pytest
12
-
13
- import read_transcript as RT
14
-
15
-
16
- def _run_cli(cli_path, *args, env_overrides=None):
17
- env = dict(os.environ)
18
- env["PYTHONIOENCODING"] = "utf-8"
19
- if env_overrides:
20
- env.update(env_overrides)
21
- return subprocess.run(
22
- [sys.executable, str(cli_path), *args],
23
- capture_output=True,
24
- text=True,
25
- encoding="utf-8",
26
- env=env,
27
- )
28
-
29
-
30
- @pytest.fixture
31
- def fake_root(fixtures_dir, tmp_path):
32
- root = tmp_path / "projects"
33
- proj = root / "C--fake-proj"
34
- proj.mkdir(parents=True)
35
- shutil.copy(fixtures_dir / "timeline-day-test.jsonl", proj / "abc12345.jsonl")
36
- return root
37
-
38
-
39
- # ── unit: gap + duration helpers ─────────────────────────────────────────────
40
-
41
- def test_parse_gap_default():
42
- assert RT._parse_gap(None) == 15
43
-
44
-
45
- def test_parse_gap_minutes():
46
- assert RT._parse_gap("20m") == 20
47
- assert RT._parse_gap("20") == 20
48
-
49
-
50
- def test_parse_gap_hours():
51
- assert RT._parse_gap("1h") == 60
52
-
53
-
54
- def test_parse_gap_invalid():
55
- with pytest.raises(ValueError):
56
- RT._parse_gap("soon")
57
-
58
-
59
- def test_fmt_dur():
60
- assert RT._fmt_dur(timedelta(minutes=8)) == "8m"
61
- assert RT._fmt_dur(timedelta(minutes=72)) == "1h12m"
62
- assert RT._fmt_dur(timedelta(seconds=30)) == "<1m"
63
-
64
-
65
- # ── unit: block grouping ─────────────────────────────────────────────────────
66
-
67
- def test_build_timeline_blocks(fake_root):
68
- from lib import parser as PR
69
- PR.set_timezone("UTC")
70
- try:
71
- data = RT.build_timeline(
72
- [fake_root / "C--fake-proj"],
73
- since=datetime(2026, 5, 1),
74
- until=datetime(2026, 5, 2),
75
- gap_minutes=15,
76
- current_uuid=None,
77
- )
78
- finally:
79
- PR.set_timezone(None)
80
- blocks = data["blocks"]
81
- assert len(blocks) == 2
82
- assert blocks[0]["start"] == datetime(2026, 5, 1, 10, 0)
83
- assert blocks[0]["end"] == datetime(2026, 5, 1, 10, 8)
84
- assert blocks[1]["start"] == datetime(2026, 5, 1, 12, 0)
85
- assert blocks[1]["end"] == datetime(2026, 5, 1, 12, 2)
86
-
87
-
88
- def test_build_timeline_wide_gap_merges(fake_root):
89
- from lib import parser as PR
90
- PR.set_timezone("UTC")
91
- try:
92
- data = RT.build_timeline(
93
- [fake_root / "C--fake-proj"],
94
- since=datetime(2026, 5, 1),
95
- until=datetime(2026, 5, 2),
96
- gap_minutes=180, # 3h gap threshold swallows the 1h52m idle
97
- current_uuid=None,
98
- )
99
- finally:
100
- PR.set_timezone(None)
101
- assert len(data["blocks"]) == 1
102
-
103
-
104
- # ── CLI ──────────────────────────────────────────────────────────────────────
105
-
106
- def test_timeline_cli_text(cli_path, fake_root):
107
- r = _run_cli(
108
- cli_path, "--root", str(fake_root), "--tz", "UTC",
109
- "--mode", "timeline", "--date", "2026-05-01",
110
- )
111
- assert r.returncode == 0
112
- assert "10:00" in r.stdout
113
- assert "12:02" in r.stdout
114
- assert "idle" in r.stdout
115
- assert "2 block(s)" in r.stdout
116
- # Timeline makes no attention claim — that's engagement mode's job.
117
- assert "active" not in r.stdout
118
-
119
-
120
- def test_timeline_cli_json(cli_path, fake_root):
121
- r = _run_cli(
122
- cli_path, "--root", str(fake_root), "--tz", "UTC",
123
- "--mode", "timeline", "--date", "2026-05-01", "--format", "json",
124
- )
125
- assert r.returncode == 0
126
- data = json.loads(r.stdout)
127
- assert data["totals"]["blocks"] == 2
128
- assert data["totals"]["sessions"] == 1
129
- assert data["totals"]["span_minutes"] == 122 # 10:00 → 12:02
130
- assert "active_minutes" not in data["totals"]
131
- assert data["blocks"][0]["start"].endswith("10:00:00")
132
- assert data["blocks"][0]["sessions"][0]["uuid"] == "abc12345"
133
-
134
-
135
- def test_timeline_cli_empty_range(cli_path, fake_root):
136
- r = _run_cli(
137
- cli_path, "--root", str(fake_root), "--tz", "UTC",
138
- "--mode", "timeline", "--date", "2020-01-01",
139
- )
140
- assert r.returncode == 0
141
- assert "no activity" in r.stdout
142
-
143
-
144
- def test_timeline_cli_project_filter(cli_path, fake_root):
145
- r = _run_cli(
146
- cli_path, "--root", str(fake_root), "--tz", "UTC",
147
- "--mode", "timeline", "--date", "2026-05-01", "--project", "fake",
148
- )
149
- assert r.returncode == 0
150
- assert "10:00" in r.stdout
151
-
152
-
153
- def test_timeline_cli_exclude_current(cli_path, fake_root):
154
- r = _run_cli(
155
- cli_path, "--root", str(fake_root), "--tz", "UTC",
156
- "--mode", "timeline", "--date", "2026-05-01", "--exclude-current",
157
- env_overrides={"CLAUDE_SESSION_ID": "abc12345"},
158
- )
159
- assert r.returncode == 0
160
- assert "no activity" in r.stdout
161
-
162
-
163
- def test_journal_cli_exclude_current(cli_path, fake_root):
164
- r = _run_cli(
165
- cli_path, "--root", str(fake_root),
166
- "--mode", "journal", "--since", "2020-01-01", "--all-projects",
167
- "--exclude-current",
168
- env_overrides={"CLAUDE_SESSION_ID": "abc12345"},
169
- )
170
- assert r.returncode == 0
171
- assert "abc12345" not in r.stdout
172
-
173
-
174
- def test_timeline_cli_project_no_match(cli_path, fake_root):
175
- r = _run_cli(
176
- cli_path, "--root", str(fake_root), "--tz", "UTC",
177
- "--mode", "timeline", "--date", "2026-05-01", "--project", "zzz-nope",
178
- )
179
- assert r.returncode == 1