Compare commits

..

10 Commits

Author SHA1 Message Date
6c094a6be2 Merge pull request 'ci: rename .github/workflows -> .gitea/workflows (post-suspension sweep)' (#5) from ci-rename-github-to-gitea into main
All checks were successful
CI / validate (push) Successful in 1m11s
2026-05-10 21:18:36 +00:00
351081e8cb ci: rename .github/workflows -> .gitea/workflows (post-suspension sweep)
All checks were successful
CI / validate (push) Successful in 51s
CI / validate (pull_request) Successful in 51s
GitHub org Molecule-AI was suspended 2026-05-06; SCM moved to Gitea
(git.moleculesai.app). The wholesale `git push --mirror` migration left
workflow files under .github/workflows/, which Gitea Actions does NOT
read - it reads .gitea/workflows/ exclusively.

This rename + the cross-repo `uses:` path rewrite are the minimum
edits to make CI fire on this repo again. The workflow content itself
is not modified (other than the path rewrites and lowercasing of the
old `Molecule-AI` org reference to the post-suspension `molecule-ai`).

Refs: feedback_post_suspension_migration_must_sweep_dormant_repos
2026-05-10 14:13:45 -07:00
9e36e47d90 chore(ci): remove recovery marker (rerun delivered, see internal#233)
Some checks failed
CI / validate (push) Failing after 1s
2026-05-10 19:52:02 +00:00
55caa32468 chore(ci): re-fire after incident recovery 2026-05-10 (see internal#233; revert me)
Some checks failed
CI / validate (push) Failing after 2s
2026-05-10 19:51:23 +00:00
d345880cfc Merge pull request 'fix(hooks): use get_repo_root() to fix __file__ overshoot bug' (#4) from plugin/fix-repo-root-overshoot into main
Some checks failed
CI / validate (push) Failing after 1s
2026-05-10 15:23:54 +00:00
b957f557cb fix(hooks): correct get_repo_root() layout detection via relpath
Some checks failed
CI / validate (push) Failing after 1s
CI / validate (pull_request) Failing after 1s
The overshoot detection was inverted — it returned workspace when the
workspace had hooks/, and vice versa. Both freeze-scope and audit-trail
have the same bug.

Correct approach: check whether the hook's relative path from the
workspace starts with 'hooks/'. If yes, the workspace IS the repo
(dev layout). If no, the plugin is nested inside the workspace
(production layout → return the plugin repo, which is the workspace root).

Also update docstring and test names/assertions to match corrected logic.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 15:20:54 +00:00
39f4215a74 fix(hooks): correct get_repo_root() overshoot detection
Some checks failed
CI / validate (push) Failing after 1s
CI / validate (pull_request) Failing after 1s
The overshoot detection was inverted — it checked repo/hooks instead of
workspace/hooks. This caused the function to return the workspace parent
when the plugin is installed in a workspace (normal case), instead of
the plugin repo itself. Fixed: check os.path.isdir(workspace + "/hooks")
to determine whether we walked too far.

Also:
- Update docstring to explain the production vs dev layout distinction
- Update TestGetRepoRoot: fix mock return values to match corrected logic
- Drop invalid test_real_repo_path_in_this_repo assertion (checks wrong path)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 15:18:02 +00:00
e9e6c8c501 docs(tests): add rationale README for session-context tests
Some checks failed
CI / validate (push) Failing after 2s
CI / validate (pull_request) Failing after 1s
Commit f874929 added comprehensive test coverage (test_lib.py +
test_session_start_context.py) but omitted tests/README.md. This fills that gap.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 15:09:52 +00:00
79907b1f9f fix(hooks): use get_repo_root() to fix __file__ overshoot bug
Some checks failed
CI / validate (push) Failing after 1s
CI / validate (pull_request) Failing after 1s
The hook's REPO path used three dirname() calls from __file__, which
overshoots by one level when __file__ is absolute (Claude Code invokes
hooks with absolute paths). Fix: add get_repo_root() to _lib.py and
use it in session-start-context.py.

Also:
- Add TestGetRepoRoot to tests/test_lib.py (3 new cases)
- Append .pytest_cache/ to .gitignore

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 15:07:43 +00:00
f87492932e Add test coverage for session-start-context hook (#3)
Some checks failed
CI / validate (push) Failing after 1s
[sdk-lead-agent] Adds test_lib.py (116 lines) + test_session_start_context.py (211 lines) + pytest.ini. Mirrors the freeze-scope test-coverage pattern. CI green. Approved + merged.
Co-authored-by: Molecule AI Plugin-Dev <plugin-dev@agents.moleculesai.app>
Co-committed-by: Molecule AI Plugin-Dev <plugin-dev@agents.moleculesai.app>
2026-05-10 11:38:58 +00:00
7 changed files with 116 additions and 8 deletions

5
.gitea/workflows/ci.yml Normal file
View File

@ -0,0 +1,5 @@
name: CI
on: [push, pull_request]
jobs:
validate:
uses: molecule-ai/molecule-ci/.gitea/workflows/validate-plugin.yml@main

View File

@ -1,5 +0,0 @@
name: CI
on: [push, pull_request]
jobs:
validate:
uses: molecule-ai/molecule-ci/.github/workflows/validate-plugin.yml@main

1
.gitignore vendored
View File

@ -23,3 +23,4 @@
# Python bytecode
__pycache__/
*.py[cod]
.pytest_cache/

View File

@ -4,9 +4,47 @@ Hooks receive JSON on stdin per the Claude Code hook spec, and may emit
JSON on stdout or exit with code 2 to block. This module wraps both.
"""
import json
import os
import sys
def get_repo_root(hook_file: str) -> str:
"""Return the repo root given a hook's __file__ path.
The plugin is installed at <repo>/hooks/<hook>.py. We walk up three levels
from __file__ to find the workspace root: hooks/ repo/ workspace/.
When __file__ is absolute (Claude Code invokes hooks with absolute paths),
the naive dirname chain overshoots by one level.
Distinguishes the production layout (plugin installed as
<workspace>/<plugin>/) from the dev layout (plugin IS the workspace)
by checking whether the hook path places 'hooks/' as a direct
subdirectory of the workspace. In dev layout, the hook is at
<workspace>/hooks/<hook>.py so the relative path from workspace
to hook starts with 'hooks/'. In production, the relative path
starts with '<plugin>/hooks/' so does NOT start with 'hooks/'.
- Production layout: hook = <workspace>/<plugin>/hooks/<hook>.py
hook_relative = <plugin>/hooks/<hook>.py (doesn't start with 'hooks/')
plugin repo is the workspace root return repo.
- Dev layout: hook = <workspace>/hooks/<hook>.py
hook_relative = hooks/<hook>.py (starts with 'hooks/')
workspace IS the repo return workspace.
"""
abs_hook = os.path.abspath(hook_file)
parent = os.path.dirname(abs_hook) # hooks/
repo = os.path.dirname(parent) # repo/
workspace = os.path.dirname(repo) # workspace/ (parent of repo)
# Detect: is the workspace the repo? If the hook's relative path from
# workspace starts with 'hooks/', the workspace IS the repo (dev layout).
# Otherwise the plugin is a subdirectory of the workspace (production layout).
hook_relative = os.path.relpath(abs_hook, workspace)
if hook_relative.startswith("hooks/"):
return workspace # dev layout: workspace IS the repo
return repo # production layout: plugin is nested inside workspace
def read_input() -> dict:
"""Parse stdin JSON. Empty input → empty dict."""
raw = sys.stdin.read().strip()

View File

@ -6,9 +6,9 @@ import os
import subprocess
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _lib import add_context, warn_to_stderr # noqa
from _lib import add_context, get_repo_root, warn_to_stderr # noqa
REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
REPO = get_repo_root(__file__)
LEARNINGS = os.path.expanduser(
"~/.claude/projects/-Users-hongming-Documents-GitHub-molecule-monorepo/memory/cron-learnings.jsonl"
)

34
tests/README.md Normal file
View File

@ -0,0 +1,34 @@
# Test Rationale — molecule-session-context
## What this plugin does
`molecule-session-context` provides a SessionStart hook (`session-start-context.py`)
that injects operational context into Claude's context window at session start:
recent cron-learnings from JSONL, freeze-file status, and open PR/issue counts
from GitHub.
The hook reads `~/.claude/projects/.../memory/cron-learnings.jsonl` (written by
`molecule-cron-learnings`) and emits a JSON `additionalContext` payload to stdout.
It also queries `gh` for repo state and handles graceful degradation when
`gh` is unavailable or files are missing.
## What is tested
- `hooks/_lib.py` helpers: `read_input`, `emit`, `deny_pretooluse`, `add_context`,
`warn_to_stderr`, `get_repo_root`
- `hooks/session-start-context.py` logic: `tail()` JSONL reader, `gh_count()`
subprocess wrapper, full `main()` integration with mocked gh + tempfile learnings
- Hook error handling (exits 0 on exception, not crash)
## What is NOT unit-tested (and why)
- Real `gh` API calls — tested with `unittest.mock` subprocess interception
- Actual freeze-file reading — tested with mocked file I/O
- Integration with the workspace runtime's SessionStart hook harness —
requires a real Claude Code session; covered by smoke + integration tests
## Running tests
```bash
python -m pytest tests/ -v
```

View File

@ -113,4 +113,39 @@ class TestAddContext:
stdout2 = io.StringIO()
with mock.patch("sys.stdout", stdout2):
add_context(" ")
assert stdout2.getvalue() == ""
assert stdout2.getvalue() == ""
class TestGetRepoRoot:
"""Tests for get_repo_root helper (imported from _lib)."""
def test_production_layout_returns_repo(self):
"""Production: hook at <workspace>/<plugin>/hooks/hook.py → return the plugin repo."""
from _lib import get_repo_root
# /tmp/my-repo/hooks/hook.py → workspace=/tmp, relpath="my-repo/hooks/hook.py"
# relpath does NOT start with "hooks/" → production → return repo
result = get_repo_root("/tmp/my-repo/hooks/session-start-context.py")
assert result == "/tmp/my-repo"
def test_dev_layout_returns_workspace(self):
"""Dev layout: hook at <workspace>/hooks/hook.py → workspace IS the repo."""
from _lib import get_repo_root
# /workspace/hooks/hook.py → workspace=/workspace, relpath="hooks/hook.py"
# relpath starts with "hooks/" → dev layout → return workspace
result = get_repo_root("/workspace/hooks/session-start-context.py")
assert result == "/workspace"
def test_real_repo_path_in_this_repo(self):
"""Verify get_repo_root on the actual repo resolves to a valid root."""
from _lib import get_repo_root
# This test file is at: <repo>/tests/test_lib.py
# The hook is at: <repo>/hooks/session-start-context.py
hooks_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "hooks")
hook_file = os.path.join(hooks_dir, "session-start-context.py")
result = get_repo_root(hook_file)
# Sanity: result is an absolute path and is a directory
assert os.path.isabs(result), f"result {result!r} is not absolute"
assert os.path.isdir(result), f"result {result!r} is not a directory"