Skip to content

Commit e5b215d

Browse files
committed
Merge chore/hardening-coverage: 0.1.1 — install fix + coverage hardening
2 parents 3771954 + a3606f4 commit e5b215d

11 files changed

Lines changed: 81 additions & 8 deletions

.claude-plugin/plugin.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "session-cost",
33
"displayName": "Session Cost Tracker",
4-
"version": "0.1.0",
4+
"version": "0.1.1",
55
"description": "Track estimated per-session Claude Code spend, report it with /session-cost:report, and optionally enforce soft/hard budget limits from ~/.claude/budgets.json",
66
"author": {
77
"name": "OpenSea",
@@ -15,6 +15,5 @@
1515
"budget",
1616
"tracking",
1717
"usage"
18-
],
19-
"hooks": "./hooks/hooks.json"
18+
]
2019
}

.covboot/sitecustomize.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import coverage
2+
coverage.process_startup()

.coverage

68 KB
Binary file not shown.

.coveragerc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[run]
2+
branch = True
3+
parallel = True
4+
source = scripts
5+
6+
[report]
7+
precision = 1
8+
show_missing = True

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,9 @@ Initial release.
1919
- Team totals via `CLAUDE_TEAM_NAME`.
2020
- Pricing table: Fable 5, Opus 4.5–4.8, Sonnet 4.5/4.6, Haiku 4.5
2121
(Anthropic published rates, 2026).
22+
23+
## 0.1.1 — 2026-06-10
24+
25+
- Fix: remove duplicate `hooks` declaration from plugin.json — `hooks/hooks.json`
26+
is auto-loaded by convention and redeclaring it fails plugin load with
27+
"Duplicate hooks file detected". Caught by live install verification.

scripts/cost-track.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ def _parse_transcript_delta(path, offset, seen_ids):
6565
continue
6666
if not isinstance(entry, dict) or entry.get("type") != "assistant":
6767
continue
68-
message = entry.get("message") or {}
68+
message = entry.get("message")
69+
if not isinstance(message, dict):
70+
continue
6971
usage = message.get("usage")
7072
if not isinstance(usage, dict):
7173
continue

scripts/session-cost.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def session_report(session_id, cost_dir, budgets_path=DEFAULT_BUDGETS_PATH, proj
104104
"""Build a spend report dict for one session, or None if unreadable."""
105105
cost_file = os.path.join(cost_dir, f"{_COST_PREFIX}{session_id}.json")
106106
data = _load_json(cost_file)
107-
if not data:
107+
if not isinstance(data, dict) or not data:
108108
return None
109109

110110
total = data.get("total_estimated_usd", 0)

tests/test_budget_guard.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,26 @@ def test_team_hard_limit_blocks(self, env):
147147
assert r.returncode == 2
148148
assert "alpha" in r.stderr
149149

150+
def test_team_soft_limit_warns(self, env):
151+
env.write_budgets({"default": {"team_soft_limit_usd": 5.0}})
152+
env.write_cost("sess-1", 3.0, team_name="alpha")
153+
env.write_cost("sess-2", 3.0, team_name="alpha")
154+
r = env.run(hook_input(), CLAUDE_TEAM_NAME="alpha")
155+
assert r.returncode == 0
156+
out = json.loads(r.stdout)
157+
assert "alpha" in out["systemMessage"]
158+
assert "$6.00" in out["systemMessage"]
159+
160+
def test_team_name_falls_back_to_cost_file(self, env):
161+
"""When CLAUDE_TEAM_NAME isn't in the env, the session's own cost
162+
file team_name still drives team enforcement."""
163+
env.write_budgets({"default": {"team_hard_limit_usd": 5.0}})
164+
env.write_cost("sess-1", 3.0, team_name="alpha")
165+
env.write_cost("sess-2", 3.0, team_name="alpha")
166+
r = env.run(hook_input()) # no CLAUDE_TEAM_NAME
167+
assert r.returncode == 2
168+
assert "alpha" in r.stderr
169+
150170

151171
class TestFailOpen:
152172
def test_malformed_stdin_allows(self, env):

tests/test_plugin_manifests.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,21 @@ def test_parses_and_has_required_fields(self):
1818
assert manifest["version"]
1919
assert manifest["license"] == "MIT"
2020

21-
def test_hooks_path_exists(self):
21+
def test_default_hooks_file_exists(self):
22+
assert (ROOT / "hooks" / "hooks.json").exists()
23+
24+
def test_manifest_does_not_redeclare_default_hooks(self):
25+
"""hooks/hooks.json is auto-loaded by convention; declaring it again
26+
in plugin.json makes Claude Code load it twice and fail at install
27+
('Duplicate hooks file detected'). manifest.hooks is only for
28+
ADDITIONAL hook files."""
2229
manifest = load(".claude-plugin/plugin.json")
23-
hooks_rel = manifest.get("hooks", "./hooks/hooks.json")
24-
assert (ROOT / hooks_rel).exists()
30+
hooks = manifest.get("hooks")
31+
if hooks is not None:
32+
paths = [hooks] if isinstance(hooks, str) else hooks
33+
assert all(
34+
p.lstrip("./") != "hooks/hooks.json" for p in paths if isinstance(p, str)
35+
)
2536

2637

2738
class TestHooksJson:

tests/test_session_cost.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ def test_missing_or_malformed_returns_none(self, cost_env):
129129
(cost_env.cost_dir / "claude-cost-bad.json").write_text("{x")
130130
assert self.report(cost_env, "bad") is None
131131

132+
def test_non_dict_cost_file_returns_none(self, cost_env):
133+
(cost_env.cost_dir / "claude-cost-listy.json").write_text("[1, 2]")
134+
assert self.report(cost_env, "listy") is None
135+
(cost_env.cost_dir / "claude-cost-str.json").write_text('"hello"')
136+
assert self.report(cost_env, "str") is None
137+
132138
def test_malformed_budgets_still_reports(self, cost_env):
133139
cost_env.budgets_path.write_text("{broken")
134140
cost_env.write_cost("s1", 1.5)

0 commit comments

Comments
 (0)