feat(plugins)!: make Sidekick exclusive to Claude Code (#7278)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -10,7 +10,12 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
REPOSITORY_ROOT = ROOT.parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from build.build import build, bundle_drift, render_template, replace_output # noqa: E402
|
||||
from build.build import ( # noqa: E402
|
||||
build,
|
||||
bundle_drift,
|
||||
render_template,
|
||||
replace_output,
|
||||
)
|
||||
from build.validate import validate_bundle # noqa: E402
|
||||
|
||||
|
||||
@@ -57,6 +62,7 @@ def test_portable_bundle_is_conformant_and_self_contained(tmp_path: Path) -> Non
|
||||
assert "env" not in server
|
||||
assert not (root / "core" / "hook_runner.py").exists()
|
||||
assert not (root / "core" / "flush_worker.py").exists()
|
||||
assert not (root / "agents").exists()
|
||||
for skill in (root / "skills").glob("*/SKILL.md"):
|
||||
frontmatter = skill.read_text(encoding="utf-8").split("---", 2)[1]
|
||||
keys = {line.split(":", 1)[0] for line in frontmatter.splitlines() if ":" in line}
|
||||
@@ -73,6 +79,19 @@ def test_native_bundle_is_self_contained(host: str, tmp_path: Path) -> None:
|
||||
assert not any(path.is_symlink() for path in root.rglob("*"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host", ["claude-code", "cursor", "codex", "kimi", "antigravity"])
|
||||
def test_only_claude_bundles_sidekick(host: str, tmp_path: Path) -> None:
|
||||
root = build(host, "native", tmp_path / host)
|
||||
if host == "claude-code":
|
||||
agent = (root / "agents" / "sidekick.md").read_text()
|
||||
assert "model: sonnet" in agent
|
||||
assert "isolation: worktree" in agent
|
||||
else:
|
||||
assert not (root / "agents").exists()
|
||||
for path in root.rglob("*.json"):
|
||||
assert "sidekick" not in path.read_text().lower()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host", ["claude-code", "cursor", "codex", "kimi", "antigravity"])
|
||||
def test_native_control_skills_select_the_host_store(host: str, tmp_path: Path) -> None:
|
||||
root = build(host, "native", tmp_path / host)
|
||||
|
||||
+42
-23
@@ -2,6 +2,7 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -11,11 +12,43 @@ import pytest
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "python"))
|
||||
|
||||
from memory_core import EvidenceStore # noqa: E402
|
||||
from memory_core import EvidenceStore, RepoContext, checkpoint_stats # noqa: E402
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host", ["claude-code", "codex", "kimi", "cursor"])
|
||||
def test_sidekick_hooks_preserve_parent_scope_and_correlate_completion(tmp_path, host):
|
||||
def test_generic_tracking_preserves_legacy_runs_and_can_finish_them(tmp_path):
|
||||
database = tmp_path / "evidence.sqlite3"
|
||||
with sqlite3.connect(database) as connection:
|
||||
connection.executescript("""
|
||||
CREATE TABLE sidekick_runs (
|
||||
repo_id TEXT NOT NULL, session_id TEXT NOT NULL,
|
||||
agent_id TEXT NOT NULL, agent_type TEXT NOT NULL,
|
||||
started_at TEXT NOT NULL, stopped_at TEXT, transcript_path TEXT,
|
||||
context_chars INTEGER NOT NULL DEFAULT 0, final_message TEXT,
|
||||
PRIMARY KEY(repo_id, session_id, agent_id)
|
||||
);
|
||||
INSERT INTO sidekick_runs VALUES
|
||||
('repo', 'session', 'worker', 'default', '2026-09-10', NULL, NULL, 123, NULL);
|
||||
""")
|
||||
for _ in range(2):
|
||||
store = EvidenceStore(database)
|
||||
try:
|
||||
assert store.status("repo")["subagent_runs"] == 1
|
||||
row = store.conn.execute("SELECT * FROM sidekick_runs").fetchone()
|
||||
assert row["agent_id"] == "worker"
|
||||
assert row["context_chars"] == 123
|
||||
repo = RepoContext(str(tmp_path), str(tmp_path), "repo", "app", "main", "")
|
||||
store.stop_subagent(repo, "session", "worker", "default", "", "Completed after upgrade.")
|
||||
assert store.status("repo")["last_subagent"]["stopped_at"]
|
||||
assert store.conn.execute("SELECT final_message FROM sidekick_runs").fetchone()[0] == (
|
||||
"Completed after upgrade."
|
||||
)
|
||||
finally:
|
||||
store.close()
|
||||
assert checkpoint_stats([{"kind": "sidekick_stop", "payload": {"final_message": "Legacy result."}}]) == (0, 1, 14)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host", ["claude-code", "codex"])
|
||||
def test_subagent_hooks_preserve_parent_scope_and_correlate_completion(tmp_path, host):
|
||||
parent = tmp_path / "parent"
|
||||
child = tmp_path / "child-worktree"
|
||||
parent.mkdir()
|
||||
@@ -30,21 +63,10 @@ def test_sidekick_hooks_preserve_parent_scope_and_correlate_completion(tmp_path,
|
||||
plugin = ROOT.parent / f"{host}-plugin"
|
||||
adapter = plugin / ("adapters/claude/hook.py" if host == "claude-code" else "hooks/adapter.py")
|
||||
start, stop = "sidekick-start", "sidekick-stop"
|
||||
payload = {"session_id": "parent-session", "cwd": str(child), "agent_id": "worker", "agent_type": "sidekick"}
|
||||
payload = {"session_id": "parent-session", "cwd": str(child), "agent_id": "worker", "agent_type": "default"}
|
||||
response_key = "last_assistant_message"
|
||||
if host == "kimi":
|
||||
start, stop = "SubagentStart", "SubagentStop"
|
||||
payload["agent_name"] = payload.pop("agent_type")
|
||||
response_key = "response"
|
||||
elif host == "cursor":
|
||||
start, stop = "subagentStart", "subagentStop"
|
||||
payload = {
|
||||
"conversation_id": "parent-session",
|
||||
"workspace_roots": [str(child)],
|
||||
"subagent_id": "worker",
|
||||
"subagent_type": "sidekick",
|
||||
}
|
||||
response_key = "summary"
|
||||
if host == "codex":
|
||||
start, stop = "subagent-start", "subagent-stop"
|
||||
|
||||
env = {key: value for key, value in os.environ.items() if not key.startswith(("MEM0_", "CLAUDE_PLUGIN_"))}
|
||||
env.update(MEM0_CODE_DATA_DIR=str(database.parent), MEM0_TELEMETRY="false", MEM0_API_URL="http://127.0.0.1:1")
|
||||
@@ -62,11 +84,8 @@ def test_sidekick_hooks_preserve_parent_scope_and_correlate_completion(tmp_path,
|
||||
return result.stdout
|
||||
|
||||
output = invoke(start, payload)
|
||||
if host == "cursor":
|
||||
assert json.loads(output) == {"permission": "allow"}
|
||||
else:
|
||||
context = output if host == "kimi" else json.loads(output)["hookSpecificOutput"]["additionalContext"]
|
||||
assert "Parent memory marker." in context
|
||||
context = json.loads(output)["hookSpecificOutput"]["additionalContext"]
|
||||
assert "Parent memory marker." in context
|
||||
assert "Foreign memory marker." not in output
|
||||
assert "Parent memory marker." not in invoke(start, payload)
|
||||
invoke(stop, {**payload, response_key: "Finished the delegated task."})
|
||||
@@ -80,4 +99,4 @@ def test_sidekick_hooks_preserve_parent_scope_and_correlate_completion(tmp_path,
|
||||
)
|
||||
assert runs[0]["stopped_at"]
|
||||
assert runs[0]["final_message"] == "Finished the delegated task."
|
||||
assert (runs[0]["context_chars"] > 0) == (host != "cursor")
|
||||
assert runs[0]["context_chars"] > 0
|
||||
Reference in New Issue
Block a user