"""Tests for .opencode/scripts/setup-memory.sh — deterministic memory init. Uses a local mock remote (``git init --bare``) instead of GitHub so the tests are hermetic and offline-safe. ``tmp_path`` provides isolation. The script is invoked via ``subprocess`` with env vars pointing at the mock remote + a tmp dir. Each test asserts one of the deterministic flow steps. Script contract (``setup-memory.sh``): 1. mkdir -p MEMORY_DIR 2. clone REMOTE | git pull --ff-only 3. remote set-url if origin != REMOTE 4. install post-commit hook (auto-push) if missing/wrong 5. memory index if .rag missing (best-effort, CLI optional) 5b. generate JS wrapper for opencode-memory plugin (idempotent + backup) 6. echo status Exit 1 when ``OPENCODE_MEMORY_REMOTE`` is unset (no default — the ``.env.example`` provides the value; absence is a config error). """ import os import subprocess import sys from pathlib import Path import pytest REPO_ROOT = Path(__file__).resolve().parent.parent SCRIPT = REPO_ROOT / ".opencode" / "scripts" / "setup-memory.sh" REPO_PYTHON = str(REPO_ROOT / ".venv" / "bin" / "python") EXPECTED_HOOK = "#!/bin/bash\ngit push origin master 2>/dev/null || true\n" def _seed_remote(remote_dir: Path) -> None: """Seed a bare remote with one commit on ``master``.""" remote_dir.mkdir(parents=True, exist_ok=True) subprocess.run(["git", "init", "--bare", "-q", str(remote_dir)], check=True) seed = remote_dir.parent / "seed" seed.mkdir() subprocess.run(["git", "init", "-q", str(seed)], check=True) (seed / "README.md").write_text("# memory\n") subprocess.run(["git", "-C", str(seed), "add", "README.md"], check=True) subprocess.run( [ "git", "-C", str(seed), "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "init", ], check=True, ) subprocess.run(["git", "-C", str(seed), "branch", "-M", "master"], check=True) subprocess.run( ["git", "-C", str(seed), "remote", "add", "origin", str(remote_dir)], check=True, ) subprocess.run(["git", "-C", str(seed), "push", "-q", "origin", "master"], check=True) def _run_script( memory_dir: Path, remote: str | None, wrapper: Path | None = None, ) -> subprocess.CompletedProcess[str]: """Run setup-memory.sh with env pointing at tmp paths. When ``remote`` is None, OPENCODE_MEMORY_REMOTE is removed from env (simulating the "no remote env" error path). ``wrapper`` sets ``MEMORY_WRAPPER_PATH`` so tests don't write to the real plugin dir. """ env = { **os.environ, "OPENCODE_MEMORY_DIR": str(memory_dir), "GIT_TERMINAL_PROMPT": "0", } if remote is not None: env["OPENCODE_MEMORY_REMOTE"] = remote else: env.pop("OPENCODE_MEMORY_REMOTE", None) if wrapper is not None: env["MEMORY_WRAPPER_PATH"] = str(wrapper) wrapper.parent.mkdir(parents=True, exist_ok=True) env["MEMORY_WRAPPER_PYTHON"] = REPO_PYTHON env["OPENCODE_WORKSPACE"] = str(REPO_ROOT) return subprocess.run( ["bash", str(SCRIPT)], capture_output=True, text=True, check=False, env=env, ) def test_fresh_init(tmp_path: Path) -> None: """Empty dir → run → clone, hook, remote, files present.""" remote = tmp_path / "remote.git" _seed_remote(remote) mem = tmp_path / "mem" r = _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js") assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" assert (mem / ".git").is_dir(), "repo not cloned" assert (mem / "README.md").exists(), "file not pulled" assert (mem / ".git" / "hooks" / "post-commit").exists(), "hook missing" got = subprocess.run( ["git", "-C", str(mem), "remote", "get-url", "origin"], capture_output=True, text=True, check=True, ).stdout.strip() assert got == str(remote), f"remote mismatch: {got}" def test_existing_repo(tmp_path: Path) -> None: """.git exists → run → pull --ff-only, no destructive changes.""" remote = tmp_path / "remote.git" _seed_remote(remote) mem = tmp_path / "mem" assert _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js").returncode == 0 # capture state before second run head_before = subprocess.run( ["git", "-C", str(mem), "rev-parse", "HEAD"], capture_output=True, text=True, check=True, ).stdout.strip() r = _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js") assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" head_after = subprocess.run( ["git", "-C", str(mem), "rev-parse", "HEAD"], capture_output=True, text=True, check=True, ).stdout.strip() assert head_before == head_after, "pull --ff-only changed HEAD unexpectedly" assert "pulling latest" in r.stdout def test_wrong_remote(tmp_path: Path) -> None: """remote ≠ expected → run → set-url corrects the remote.""" remote = tmp_path / "remote.git" _seed_remote(remote) mem = tmp_path / "mem" assert _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js").returncode == 0 # corrupt remote URL (tmp path — avoids S108 insecure-tmp warning) wrong = tmp_path / "wrong-remote" subprocess.run( ["git", "-C", str(mem), "remote", "set-url", "origin", str(wrong)], check=True, ) r = _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js") assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" assert "fixing remote" in r.stdout got = subprocess.run( ["git", "-C", str(mem), "remote", "get-url", "origin"], capture_output=True, text=True, check=True, ).stdout.strip() assert got == str(remote), f"remote not fixed: {got}" def test_missing_hook(tmp_path: Path) -> None: """hook missing → run → hook created with correct content.""" remote = tmp_path / "remote.git" _seed_remote(remote) mem = tmp_path / "mem" assert _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js").returncode == 0 hook = mem / ".git" / "hooks" / "post-commit" hook.unlink() assert not hook.exists() r = _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js") assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" assert hook.exists(), "hook not recreated" assert hook.read_text() == EXPECTED_HOOK, f"hook content wrong: {hook.read_text()!r}" assert "installing post-commit hook" in r.stdout def test_idempotent(tmp_path: Path) -> None: """run 3x → state identical after run 1 and run 3.""" remote = tmp_path / "remote.git" _seed_remote(remote) mem = tmp_path / "mem" def _snapshot() -> dict[str, str | None]: head = subprocess.run( ["git", "-C", str(mem), "rev-parse", "HEAD"], capture_output=True, text=True, check=True, ).stdout.strip() hook = mem / ".git" / "hooks" / "post-commit" return { "head": head, "hook": hook.read_text() if hook.exists() else None, "remote": subprocess.run( ["git", "-C", str(mem), "remote", "get-url", "origin"], capture_output=True, text=True, check=True, ).stdout.strip(), } assert _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js").returncode == 0 snap1 = _snapshot() assert _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js").returncode == 0 assert _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js").returncode == 0 snap3 = _snapshot() assert snap1 == snap3, f"non-idempotent:\n run1={snap1}\n run3={snap3}" def test_no_remote_env(tmp_path: Path) -> None: """OPENCODE_MEMORY_REMOTE unset → exit 1, error message to stderr/stdout.""" mem = tmp_path / "mem" r = _run_script(mem, remote=None) assert r.returncode == 1, f"expected exit 1, got {r.returncode}" combined = r.stderr + r.stdout assert "OPENCODE_MEMORY_REMOTE" in combined, f"missing error msg: {combined!r}" assert "ERROR" in combined or "not set" in combined # ── git insteadOf helper (GITHUB_TOKEN) ────────────────────────────────────── def test_script_contains_git_insteadof() -> None: """setup-memory.sh contains `git config --global url.insteadOf` before clone.""" src = SCRIPT.read_text() assert "git config --global url." in src, "missing git config --global url. call" assert ".insteadOf https://github.com/" in src, "missing insteadOf directive" # The insteadOf block must appear BEFORE the git clone line. insteadof_pos = src.index(".insteadOf") clone_pos = src.index("git clone --origin") assert insteadof_pos < clone_pos, "insteadOf helper must precede git clone" def test_script_contains_github_token_check() -> None: """setup-memory.sh checks GITHUB_TOKEN (guarded, not hard-fail if empty).""" src = SCRIPT.read_text() assert "GITHUB_TOKEN" in src, "missing GITHUB_TOKEN reference" # Must be a conditional check (if [ -n ... ]), not an unconditional hard fail. assert '-n "${GITHUB_TOKEN:-}"' in src or '-n "$GITHUB_TOKEN"' in src, ( "GITHUB_TOKEN must be a guarded check (if [ -n ... ]), not a hard requirement" ) def test_insteadof_idempotent(tmp_path: Path) -> None: """Running the script N times does not duplicate the insteadOf entry. `git config --global url..insteadOf` overwrites the value on each call (idempotent by git semantics). We verify by invoking setup-memory.sh twice with a fake GITHUB_TOKEN against a mock remote and checking that `git config --global --get-all` returns exactly one match after each run. """ remote = tmp_path / "remote.git" _seed_remote(remote) mem = tmp_path / "mem" # Use an isolated HOME so --global writes to a tmp gitconfig (no pollution). home = tmp_path / "home" home.mkdir() env = { **os.environ, "OPENCODE_MEMORY_DIR": str(mem), "OPENCODE_MEMORY_REMOTE": str(remote), "GITHUB_TOKEN": "fake-token-for-test", "HOME": str(home), "GIT_TERMINAL_PROMPT": "0", "MEMORY_WRAPPER_PATH": str(tmp_path / "wrapper" / "rag.js"), "MEMORY_WRAPPER_PYTHON": REPO_PYTHON, "OPENCODE_WORKSPACE": str(REPO_ROOT), } (tmp_path / "wrapper").mkdir(exist_ok=True) # First run: clone path triggers the insteadOf helper. r1 = subprocess.run(["bash", str(SCRIPT)], capture_output=True, text=True, check=False, env=env) assert r1.returncode == 0, f"run1 failed: stdout={r1.stdout}\nstderr={r1.stderr}" # Read the global gitconfig written by the script. gitconfig = home / ".gitconfig" assert gitconfig.exists(), f"gitconfig not created at {gitconfig}" content1 = gitconfig.read_text() assert "x-access-token:fake-token-for-test@github.com" in content1 assert "insteadOf" in content1 # Count insteadOf occurrences — should be exactly 1 (no duplication). insteadof_count_1 = content1.count("insteadOf") assert insteadof_count_1 == 1, f"expected 1 insteadOf after run1, got {insteadof_count_1}" # Second run: existing repo → pull path (clone branch skipped). The helper # is inside the clone branch, so it won't re-fire — but we assert the # config is NOT duplicated regardless (single insteadOf entry preserved). r2 = subprocess.run(["bash", str(SCRIPT)], capture_output=True, text=True, check=False, env=env) assert r2.returncode == 0, f"run2 failed: stdout={r2.stdout}\nstderr={r2.stderr}" content2 = gitconfig.read_text() insteadof_count_2 = content2.count("insteadOf") assert insteadof_count_2 == 1, ( f"expected 1 insteadOf after run2, got {insteadof_count_2} (duplicate?)" ) def test_no_github_token_skips_insteadof(tmp_path: Path) -> None: """When GITHUB_TOKEN is unset, the insteadOf helper is skipped (no error).""" remote = tmp_path / "remote.git" _seed_remote(remote) mem = tmp_path / "mem" home = tmp_path / "home" home.mkdir() env = { **os.environ, "OPENCODE_MEMORY_DIR": str(mem), "OPENCODE_MEMORY_REMOTE": str(remote), "HOME": str(home), "GIT_TERMINAL_PROMPT": "0", "MEMORY_WRAPPER_PATH": str(tmp_path / "wrapper" / "rag.js"), "MEMORY_WRAPPER_PYTHON": REPO_PYTHON, "OPENCODE_WORKSPACE": str(REPO_ROOT), } env.pop("GITHUB_TOKEN", None) (tmp_path / "wrapper").mkdir(exist_ok=True) r = subprocess.run(["bash", str(SCRIPT)], capture_output=True, text=True, check=False, env=env) assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" gitconfig = home / ".gitconfig" if gitconfig.exists(): content = gitconfig.read_text() assert "insteadOf" not in content, "insteadOf set without GITHUB_TOKEN" # ── JS wrapper for opencode-memory plugin (step 5b) ────────────────────────── def test_wrapper_generated(tmp_path: Path) -> None: """Run → wrapper file exists at MEMORY_WRAPPER_PATH with delegate content.""" remote = tmp_path / "remote.git" _seed_remote(remote) mem = tmp_path / "mem" wrapper = tmp_path / "wrapper" / "rag.js" r = _run_script(mem, str(remote), wrapper=wrapper) assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" assert wrapper.exists(), f"wrapper not generated at {wrapper}" assert wrapper.stat().st_mode & 0o100, "wrapper not executable" assert "generated wrapper" in r.stdout, f"missing generate msg: {r.stdout!r}" def test_wrapper_idempotent(tmp_path: Path) -> None: """Run 2x → wrapper content identical, 2nd run reports 'wrapper correct'.""" remote = tmp_path / "remote.git" _seed_remote(remote) mem = tmp_path / "mem" wrapper = tmp_path / "wrapper" / "rag.js" r1 = _run_script(mem, str(remote), wrapper=wrapper) assert r1.returncode == 0, f"run1: stdout={r1.stdout}\nstderr={r1.stderr}" content1 = wrapper.read_text() r2 = _run_script(mem, str(remote), wrapper=wrapper) assert r2.returncode == 0, f"run2: stdout={r2.stdout}\nstderr={r2.stderr}" content2 = wrapper.read_text() assert content1 == content2, "wrapper content changed between runs" assert "wrapper correct" in r2.stdout, f"2nd run should report correct: {r2.stdout!r}" def test_wrapper_content(tmp_path: Path) -> None: """Wrapper contains python memory CLI delegation + spawnSync.""" remote = tmp_path / "remote.git" _seed_remote(remote) mem = tmp_path / "mem" wrapper = tmp_path / "wrapper" / "rag.js" r = _run_script(mem, str(remote), wrapper=wrapper) assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" body = wrapper.read_text() assert "python" in body, f"wrapper missing python ref: {body!r}" assert "-m" in body and "src.memory" in body, "wrapper missing src.memory module" assert "spawnSync" in body, "wrapper missing spawnSync" assert "process.argv.slice" in body, "wrapper missing argv passthrough" def test_wrapper_backup_original(tmp_path: Path) -> None: """Pre-existing wrapper with different content → .orig backup saved once.""" remote = tmp_path / "remote.git" _seed_remote(remote) mem = tmp_path / "mem" wrapper = tmp_path / "wrapper" / "rag.js" wrapper.parent.mkdir(parents=True, exist_ok=True) original = "# old rag-cli shim\nconsole.log('old');\n" wrapper.write_text(original) r = _run_script(mem, str(remote), wrapper=wrapper) assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" backup = wrapper.with_suffix(".js.orig") assert backup.exists(), ".orig backup not created" assert backup.read_text() == original, ".orig does not preserve original content" assert wrapper.read_text() != original, "wrapper not replaced with delegate" # Second run with different wrong content: .orig must NOT be overwritten. wrapper.write_text("# another wrong\n") r2 = _run_script(mem, str(remote), wrapper=wrapper) assert r2.returncode == 0, f"run2: stdout={r2.stdout}\nstderr={r2.stderr}" assert backup.read_text() == original, ".orig overwritten on 2nd backup" if __name__ == "__main__": sys.exit(pytest.main([__file__, "-v"]))