diff --git a/README.md b/README.md index d6870ed..45d5f6e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ # AI Code Guard -Guardrails for AI coding agents. One `guard.yaml` constrains behavior and enforces code quality across Claude Code, Cursor, OpenCode, Copilot, and KiloCode. +Guardrails for AI coding agents. One `guard.yaml` constrains behavior and enforces code quality across Claude Code, OpenCode, Copilot, and KiloCode. A single config file replaces per-agent rule documents, scattered hook scripts, and manual pre-commit setup. @@ -42,7 +42,6 @@ New here? Walk through the full flow in the | Agent | Runtime Hook | Code Quality | Rule Document | |-------|:---:|:---:|---| | Claude Code | deny + ask | yes | `CLAUDE.md` | -| Cursor | deny | yes | `.cursor/rules/behavior.mdc` | | OpenCode | deny + ask | yes | `AGENTS.md` | | GitHub Copilot | — | yes | `.github/copilot-instructions.md` | | KiloCode | — | yes | `.kilocode/rules/behavior.md` | diff --git a/README_zh.md b/README_zh.md index 8425705..3e49659 100644 --- a/README_zh.md +++ b/README_zh.md @@ -5,7 +5,7 @@ # AI Code Guard -AI 编码 Agent 的看护系统。一份 `guard.yaml` 约束行为并保障代码质量,适配 Claude Code、Cursor、OpenCode、Copilot 和 KiloCode。 +AI 编码 Agent 的看护系统。一份 `guard.yaml` 约束行为并保障代码质量,适配 Claude Code、OpenCode、Copilot 和 KiloCode。 一个配置文件替代分散的 Agent 规则文档、零散的 Hook 脚本和手动的 pre-commit 配置。 @@ -42,7 +42,6 @@ ac-guard ruleset fetch #v1.0 # 拉取共享规则集 | Agent | 运行时 Hook | 代码质量 | 规则文档 | |-------|:---:|:---:|---| | Claude Code | deny + ask | 是 | `CLAUDE.md` | -| Cursor | deny | 是 | `.cursor/rules/behavior.mdc` | | OpenCode | deny + ask | 是 | `AGENTS.md` | | GitHub Copilot | — | 是 | `.github/copilot-instructions.md` | | KiloCode | — | 是 | `.kilocode/rules/behavior.md` | diff --git a/design/AI_GUARD_SYSTEM_DESIGN.md b/design/AI_GUARD_SYSTEM_DESIGN.md index ff26385..6eae8cd 100644 --- a/design/AI_GUARD_SYSTEM_DESIGN.md +++ b/design/AI_GUARD_SYSTEM_DESIGN.md @@ -29,13 +29,13 @@ ### 1.1 背景与动机 -大语言模型(LLM)驱动的 AI 编码 Agent(如 Claude Code、Cursor、OpenCode 等)正在深刻改变软件开发流程。这些 Agent 具备文件读写、命令执行、代码生成等能力,能够显著提升开发效率。然而,Agent 的高度自主性也带来了两类系统性风险: +大语言模型(LLM)驱动的 AI 编码 Agent(如 Claude Code、OpenCode 等)正在深刻改变软件开发流程。这些 Agent 具备文件读写、命令执行、代码生成等能力,能够显著提升开发效率。然而,Agent 的高度自主性也带来了两类系统性风险: **行为风险**。AI Agent 可能执行超出预期的操作,包括但不限于:修改敏感配置文件、执行破坏性命令(如 `git push --force`)、绕过代码检查机制(如 `git commit --no-verify`)、访问受限资源等。这些行为在缺乏约束的环境中难以被及时发现和阻止。 **质量风险**。AI 生成的代码可能不符合项目编码规范(命名、格式、文档要求),引入架构层面的耦合(违反模块依赖规则),或缺少充分的测试覆盖。这些质量问题在 AI 高速生成代码的背景下尤为突出——人工 Review 的速度难以匹配 AI 的产出速度。 -当前,开发者通常依赖以下手段应对上述风险:手动配置各 Agent 的规则文件(如 CLAUDE.md、.cursor/rules/)、依赖 Agent 自身的权限系统、以及依靠人工 Code Review。这些手段存在三个共性缺陷:**规则分散**(不同 Agent 各自维护,难以统一)、**缺乏强制性**(规则文档仅为引导,不具备拦截能力)、**无法复用**(项目间无法共享规则集)。 +当前,开发者通常依赖以下手段应对上述风险:手动配置各 Agent 的规则文件(如 CLAUDE.md、AGENTS.md)、依赖 Agent 自身的权限系统、以及依靠人工 Code Review。这些手段存在三个共性缺陷:**规则分散**(不同 Agent 各自维护,难以统一)、**缺乏强制性**(规则文档仅为引导,不具备拦截能力)、**无法复用**(项目间无法共享规则集)。 AI Guard 正是为解决上述问题而设计的系统。 @@ -94,7 +94,6 @@ AI Guard 是一个面向 AI 编码 Agent 的**看护系统**(Guardian System | Agent | 核心能力 | Hook 机制 | 约束方式 | |---|---|---|---| | Claude Code | 文件读写、命令执行、MCP 调用 | PreToolUse / PostToolUse Hook | 规则文档(CLAUDE.md)+ Hook 拦截 | -| Cursor | 文件编辑、命令执行、Web 搜索 | 多类型 Hook(beforeShellExecution 等) | 规则文件(.cursor/rules/)+ Hook | | OpenCode | 文件读写、命令执行、MCP 调用 | 插件内事件系统 | 规则文档(AGENTS.md)+ 插件拦截 | | GitHub Copilot | 代码补全、文件编辑 | 无 Hook 机制 | 规则文档(copilot-instructions.md) | | KiloCode | 文件读写、命令执行 | 无 Hook 机制 | 规则文档(.kilocode/rules/) | @@ -103,7 +102,7 @@ AI Guard 是一个面向 AI 编码 Agent 的**看护系统**(Guardian System 1. **约束能力的碎片化**。不同 Agent 的 Hook 机制互不兼容,规则文档格式各异。同一套约束规则需要在每个 Agent 上分别配置,维护成本随 Agent 数量线性增长。 -2. **约束强度的不均衡**。具备 Hook 机制的 Agent(Claude Code、Cursor、OpenCode)可以实现运行时拦截,但无 Hook 机制的 Agent(Copilot、KiloCode)仅能通过规则文档进行"软约束",无法强制阻止违规操作。 +2. **约束强度的不均衡**。具备 Hook 机制的 Agent(Claude Code、OpenCode)可以实现运行时拦截,但无 Hook 机制的 Agent(Copilot、KiloCode)仅能通过规则文档进行"软约束",无法强制阻止违规操作。 ### 2.2 现有工具与方案对比 @@ -196,7 +195,7 @@ AI Guard 旨在解决 AI 辅助编码过程中的五类核心问题: **S3 — 提交质量门禁**。AI 生成的代码在 `git commit` 时自动经过格式化、命名等静态检查,在 `git push` 时经过测试、覆盖率等动态验证。 -**S4 — 多 Agent 统一适配**。同一份 guard.yaml 配置可同时适配 Claude Code、Cursor 等不同 Agent,生成各自格式的规则文档和 Hook 配置。 +**S4 — 多 Agent 统一适配**。同一份 guard.yaml 配置可同时适配 Claude Code、OpenCode 等不同 Agent,生成各自格式的规则文档和 Hook 配置。 **S5 — 规则集复用**。规则集作者维护一套公司规则,各项目通过 Git 仓库引用直接使用,项目配置可覆盖规则集的部分设置。 @@ -786,7 +785,6 @@ install 为增量操作:新增 Agent 追加至已有列表,为全部 Agent | 适配器 | Agent | can_block | can_ask | 规则文档路径 | |---|---|---|---|---| | ClaudeCodeAdapter | Claude Code | ✅ | ✅ | `CLAUDE.md` | -| CursorAdapter | Cursor | ✅ | ⚠️ 部分 | `.cursor/rules/behavior.mdc` | | OpenCodeAdapter | OpenCode | ✅ | ✅ | `AGENTS.md` | | CopilotAdapter | GitHub Copilot | ❌ | ❌ | `.github/copilot-instructions.md` | | KiloCodeAdapter | KiloCode | ❌ | ❌ | `.kilocode/rules/behavior.md` | @@ -1030,7 +1028,6 @@ Enforcer → Reporter.append_audit_log(PolicyDecision) | Agent | Hook 入口 | 输入格式 | 输出格式 | |---|---|---|---| | Claude Code | `.claude/hooks/interceptor.py`(stdin JSON) | `{"tool_name": "...", "tool_input": {...}}` | `{"hookSpecificOutput": {"permissionDecision": "deny/allow/ask"}}` | -| Cursor | `.cursor/hooks/check.sh`(stdin JSON) | Cursor 特定格式 | `{"permission": "deny"}` | | OpenCode | `.opencode/plugins/ai-guard.ts`(插件 API) | `{tool, args}` | `throw Error("...")` 阻止 | | Copilot / KiloCode | 无 Hook | — | — | @@ -1131,7 +1128,7 @@ class ResolvedConfig: ```json { "ai_guard_version": "0.1.0", - "installed_agents": ["claude-code", "cursor"], + "installed_agents": ["claude-code", "opencode"], "config_hash": "a3f8c2e1", "installed_at": "2026-04-13T10:30:00", "artifacts": ["CLAUDE.md", ".claude/settings.json", ".pre-commit-config.yaml"] @@ -1435,7 +1432,7 @@ class FileSpec: #### 12.1.3 规则文档约定 -输出必须包含托管块标记 `` / ``。Agent 特定格式差异(如 Cursor 的 `.mdc` frontmatter)由适配器自行处理。 +输出必须包含托管块标记 `` / ``。Agent 特定格式差异由适配器自行处理。 #### 12.1.4 Hook 脚本协议 @@ -1686,7 +1683,6 @@ Skill(AI Agent 按规范自动生成扩展代码) | Agent | Hook 机制 | can_block | can_ask | 规则文档 | |---|---|---|---|---| | Claude Code | PreToolUse Hook | ✅ | ✅ | CLAUDE.md | -| Cursor | 多类型 Hook | ✅ | ⚠️ 部分 | .cursor/rules/ | | OpenCode | 插件事件系统 | ✅ | ✅ | AGENTS.md | | GitHub Copilot | 无 | ❌ | ❌ | copilot-instructions.md | | KiloCode | 无 | ❌ | ❌ | .kilocode/rules/ | diff --git a/docs/getting-started.md b/docs/getting-started.md index ba8ea50..f191b74 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -191,7 +191,6 @@ Agent Block Ask Rule Doc Status ------------------------------------------------------------------------ claude-code yes yes CLAUDE.md INSTALLED copilot no no .github/copilot-instructions.md - -cursor yes no .cursor/rules/behavior.mdc - kilocode no no .kilocode/rules/behavior.md - opencode yes yes AGENTS.md - ``` diff --git a/docs/getting-started_zh.md b/docs/getting-started_zh.md index 2e332af..6040c19 100644 --- a/docs/getting-started_zh.md +++ b/docs/getting-started_zh.md @@ -181,7 +181,6 @@ Agent Block Ask Rule Doc Status ------------------------------------------------------------------------ claude-code yes yes CLAUDE.md INSTALLED copilot no no .github/copilot-instructions.md - -cursor yes no .cursor/rules/behavior.mdc - kilocode no no .kilocode/rules/behavior.md - opencode yes yes AGENTS.md - ``` diff --git a/src/ac_guard/action_guard/cli.py b/src/ac_guard/action_guard/cli.py index f1b0585..aaeaccc 100644 --- a/src/ac_guard/action_guard/cli.py +++ b/src/ac_guard/action_guard/cli.py @@ -1,14 +1,14 @@ """Action guard CLI entry point for subprocess invocation. Provides a thin JSON stdin/stdout interface for non-Python hooks -(Cursor bash, OpenCode TypeScript) to call the Action guard engine. +(OpenCode TypeScript) to call the Action guard engine. Usage: echo '{"tool_name": "Write", "tool_input": {"file_path": "x.py"}}' | \ python3 -m ac_guard.action_guard Input (stdin JSON): - {"tool_name": "...", "tool_input": {...}, "agent": "cursor", "project_root": "."} + {"tool_name": "...", "tool_input": {...}, "agent": "opencode", "project_root": "."} Only ``tool_name`` and ``tool_input`` are required. ``agent`` is recorded in the audit log to identify the caller; ``project_root`` defaults to ``.``. diff --git a/src/ac_guard/action_guard/core.py b/src/ac_guard/action_guard/core.py index dab46f6..b959ecb 100644 --- a/src/ac_guard/action_guard/core.py +++ b/src/ac_guard/action_guard/core.py @@ -57,7 +57,7 @@ def evaluate( tool_input: Tool arguments dict. project_root: Path to project root directory. agent: Identifier of the invoking agent (``"claude-code"``, - ``"cursor"``, ``"opencode"``, ...). Empty string is + ``"opencode"``, ...). Empty string is accepted for backward-compatible callers but leaves the audit record's ``agent`` field empty. diff --git a/src/ac_guard/adapters/__init__.py b/src/ac_guard/adapters/__init__.py index c685823..73421ac 100644 --- a/src/ac_guard/adapters/__init__.py +++ b/src/ac_guard/adapters/__init__.py @@ -13,7 +13,6 @@ time): - ``claude-code`` (can_block=True, can_ask=True) -- ``cursor`` (can_block=True, can_ask=False) - ``opencode`` (can_block=True, can_ask=True) - ``copilot`` (can_block=False, can_ask=False) - ``kilocode`` (can_block=False, can_ask=False) diff --git a/src/ac_guard/adapters/_templates/hooks/cursor.j2 b/src/ac_guard/adapters/_templates/hooks/cursor.j2 deleted file mode 100644 index 8db6cad..0000000 --- a/src/ac_guard/adapters/_templates/hooks/cursor.j2 +++ /dev/null @@ -1,50 +0,0 @@ -{# -Cursor Hook template (shell script). - -Generates a bash script that: -1. Reads stdin JSON (Cursor format) -2. Calls Action guard via python3 -m ac_guard.action_guard subprocess -3. Outputs stdout JSON (Cursor format) - -This template is rendered by adapters/_render.render_hook("cursor", behavior). -#} -#!/bin/bash -# Cursor Hook for AI Code Guard -# -# Generated by `ac-guard install --agent cursor`. -# This file is managed by AI Code Guard — do not edit manually. -# -# Cursor hook format: -# - Input: stdin JSON (tool/command + args) -# - Output: stdout JSON {"permission": "allow/deny"} -# - Exit code: 0 (non-zero may cause Cursor issues) - -set -e - -# Read input from stdin (Cursor format varies by hook type) -input=$(cat) - -# Parse tool call (Cursor uses different formats for different hooks) -tool_name=$(echo "$input" | jq -r '.tool // .command // .name // "unknown"' 2>/dev/null || echo "unknown") - -# Build Action guard input JSON (agent identifies the caller in audit records) -action_guard_input=$(jq -n \ - --arg tn "$tool_name" \ - --argjson ti "$(echo "$input" | jq '.args // .arguments // {}' 2>/dev/null || echo '{}')" \ - '{tool_name: $tn, tool_input: $ti, agent: "cursor"}') - -# Call Action guard via Python subprocess. -# python_executable is baked at install time so the hook works even when -# Cursor launches it from a shell whose $PATH lacks the project's venv. -action_guard_output=$(echo "$action_guard_input" | "{{ python_executable }}" -m ac_guard.action_guard 2>/dev/null || echo '{"decision": "deny"}') - -# Extract decision -decision=$(echo "$action_guard_output" | jq -r '.decision // "deny"' 2>/dev/null || echo "deny") - -# Cursor does not support "ask" — downgrade to "deny" -if [ "$decision" = "ask" ]; then - decision="deny" -fi - -# Output Cursor expected format -echo "{\"permission\": \"$decision\"}" diff --git a/src/ac_guard/adapters/_templates/rule_docs/cursor.md.j2 b/src/ac_guard/adapters/_templates/rule_docs/cursor.md.j2 deleted file mode 100644 index d7d703b..0000000 --- a/src/ac_guard/adapters/_templates/rule_docs/cursor.md.j2 +++ /dev/null @@ -1,30 +0,0 @@ -{# -Cursor rule document template. - -Cursor uses .mdc format with YAML frontmatter. -Extends base.md.j2 but overrides frontmatter and require_approval section -since Cursor has limited ask capability (can_ask=False). - -Cursor supports: -- Runtime interception (can_block=True) -- Limited ask capability - require_approval treated as deny -#} - -{% extends "rule_docs/base.md.j2" %} - -{% block frontmatter %} ---- -globs: **/* ---- -{% endblock %} - -{% block require_approval_section scoped %} -> **Note**: This Agent does not support user confirmation prompts. -> Require-approval rules are treated as forbidden. - -**Require Approval** → **Blocked** (no ask capability): -{% for rule in op_rules.require_approval %} -- `{{ rule.pattern }}`{% if rule.message %} — {{ rule.message }}{% endif %} -{% endfor %} - -{% endblock %} diff --git a/src/ac_guard/adapters/base.py b/src/ac_guard/adapters/base.py index 28f6667..9757eaa 100644 --- a/src/ac_guard/adapters/base.py +++ b/src/ac_guard/adapters/base.py @@ -63,7 +63,7 @@ def name(self) -> str: """Agent identifier used in --agent CLI argument. Must be lowercase with hyphens between words. - Examples: "claude-code", "cursor", "opencode". + Examples: "claude-code", "opencode". """ @property @@ -89,7 +89,6 @@ def rule_doc_path(self) -> str: Examples: - "CLAUDE.md" for Claude Code - - ".cursor/rules/behavior.mdc" for Cursor - ".github/copilot-instructions.md" for GitHub Copilot """ diff --git a/src/ac_guard/adapters/builtins/__init__.py b/src/ac_guard/adapters/builtins/__init__.py index 4ab8fab..7151cda 100644 --- a/src/ac_guard/adapters/builtins/__init__.py +++ b/src/ac_guard/adapters/builtins/__init__.py @@ -15,14 +15,12 @@ from ac_guard.adapters.builtins.claude_code import ClaudeCodeAdapter from ac_guard.adapters.builtins.copilot import CopilotAdapter -from ac_guard.adapters.builtins.cursor import CursorAdapter from ac_guard.adapters.builtins.kilocode import KiloCodeAdapter from ac_guard.adapters.builtins.opencode import OpenCodeAdapter __all__ = [ "ClaudeCodeAdapter", "CopilotAdapter", - "CursorAdapter", "KiloCodeAdapter", "OpenCodeAdapter", ] diff --git a/src/ac_guard/adapters/builtins/cursor.py b/src/ac_guard/adapters/builtins/cursor.py deleted file mode 100644 index e9c9db2..0000000 --- a/src/ac_guard/adapters/builtins/cursor.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Cursor Agent adapter implementation. - -Cursor supports Hook capability with limitations: -- Runtime interception (can_block=True) -- Limited ask capability (can_ask=False — treated as deny) - -Rule document: .cursor/rules/behavior.mdc (Cursor's .mdc format) -Hook entry: .cursor/hooks/check.sh (stdin/stdout JSON) -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from ac_guard.adapters._render import render_hook, render_rule_doc -from ac_guard.adapters.base import AgentAdapter, AgentCapabilities -from ac_guard.domain import FileSpec - -if TYPE_CHECKING: - from ac_guard.config import BehaviorConfig - -__all__ = ["CursorAdapter"] - - -class CursorAdapter(AgentAdapter): - """Adapter for Cursor AI coding agent.""" - - @property - def name(self) -> str: - return "cursor" - - @property - def capabilities(self) -> AgentCapabilities: - # Cursor has limited ask capability, treated as False - return AgentCapabilities(can_block=True, can_ask=False) - - def rule_doc_path(self) -> str: - return ".cursor/rules/behavior.mdc" - - def render_rule_doc(self, behavior: BehaviorConfig) -> str: - """Render behavior rules as Cursor rule document. - - Returns raw ``.mdc`` content (with frontmatter) without managed - block markers. The writer layer owns marker wrapping. - """ - return render_rule_doc(self, behavior) - - def hook_files(self, behavior: BehaviorConfig) -> list[FileSpec]: - """Generate Cursor Hook script. - - Uses Jinja2 template (hooks/cursor.j2) for the Hook script. - Returns a shell script that: - - Reads stdin JSON (Cursor format) - - Calls Action guard for policy decision (placeholder for WP2) - - Returns JSON output - """ - hook_content = render_hook(self, behavior) - - return [ - FileSpec( - path=".cursor/hooks/check.sh", - content=hook_content, - executable=True, - ), - ] diff --git a/src/ac_guard/adapters/registry.py b/src/ac_guard/adapters/registry.py index f894c66..b31aabe 100644 --- a/src/ac_guard/adapters/registry.py +++ b/src/ac_guard/adapters/registry.py @@ -20,7 +20,6 @@ from ac_guard.adapters.builtins import ( ClaudeCodeAdapter, CopilotAdapter, - CursorAdapter, KiloCodeAdapter, OpenCodeAdapter, ) @@ -50,7 +49,6 @@ def __init__(self, name: str, available: list[str]) -> None: _BUILTINS: Final[tuple[AgentAdapter, ...]] = ( ClaudeCodeAdapter(), - CursorAdapter(), OpenCodeAdapter(), CopilotAdapter(), KiloCodeAdapter(), diff --git a/src/ac_guard/cli/init.py b/src/ac_guard/cli/init.py index 9e27272..ca4608e 100644 --- a/src/ac_guard/cli/init.py +++ b/src/ac_guard/cli/init.py @@ -194,4 +194,4 @@ def init_command(request: InitRequest) -> None: print(" 1. Review and edit the configuration file") print(" 2. Run 'ac-guard install --agent ' to generate artifacts") print("\nAvailable agents:") - print(" claude-code, cursor, opencode, copilot, kilocode") + print(" claude-code, opencode, copilot, kilocode") diff --git a/src/ac_guard/cli/main.py b/src/ac_guard/cli/main.py index b8f71d1..bc877ad 100644 --- a/src/ac_guard/cli/main.py +++ b/src/ac_guard/cli/main.py @@ -129,7 +129,7 @@ def install( typer.Option( "--agent", "-a", - help="Comma-separated agent names (e.g., claude-code,cursor)", + help="Comma-separated agent names (e.g., claude-code,opencode)", ), ] = None, config: Annotated[ diff --git a/src/ac_guard/reporter/metrics.py b/src/ac_guard/reporter/metrics.py index 2301cc6..a806e15 100644 --- a/src/ac_guard/reporter/metrics.py +++ b/src/ac_guard/reporter/metrics.py @@ -243,7 +243,6 @@ def _check_label(name: str) -> str | None: ".ac-guard/", ".git/hooks/", ".claude/", - ".cursor/", "pyproject.toml", ] diff --git a/tests/integration/test_action_guard_e2e.py b/tests/integration/test_action_guard_e2e.py index 2caea7a..ba94c7e 100644 --- a/tests/integration/test_action_guard_e2e.py +++ b/tests/integration/test_action_guard_e2e.py @@ -7,8 +7,6 @@ from __future__ import annotations import json -import os -import re from pathlib import Path import yaml @@ -100,27 +98,11 @@ def test_claude_code_hook_has_action_guard(self, tmp_path: Path) -> None: content = hook_path.read_text(encoding="utf-8") assert "from ac_guard.action_guard.core import evaluate" in content - def test_cursor_hook_calls_action_guard(self, tmp_path: Path) -> None: - """Cursor hook invokes ac_guard.action_guard via the baked python.""" - _init_and_install(tmp_path, agents="cursor") - hook_path = tmp_path / ".cursor" / "hooks" / "check.sh" - assert hook_path.is_file() - content = hook_path.read_text(encoding="utf-8") - assert "-m ac_guard.action_guard" in content - # Path must be baked absolute (not a bare `python3`) so the hook - # works without the project venv on PATH. - assert "| python3 -m" not in content - match = re.search(r'\| "([^"]+)" -m ac_guard\.action_guard', content) - assert match is not None, "cursor hook missing baked python invocation" - assert os.path.isabs(match.group(1)), ( - f"baked python path is not absolute: {match.group(1)!r}" - ) - def test_multi_agent_hooks(self, tmp_path: Path) -> None: """Multi-agent install generates hooks for each agent.""" - _init_and_install(tmp_path, agents="claude-code,cursor") + _init_and_install(tmp_path, agents="claude-code,opencode") assert (tmp_path / ".claude" / "hooks" / "interceptor.py").is_file() - assert (tmp_path / ".cursor" / "hooks" / "check.sh").is_file() + assert (tmp_path / ".opencode" / "plugins" / "ac-guard.ts").is_file() class TestAuditLoggingE2E: diff --git a/tests/integration/test_cli_lifecycle.py b/tests/integration/test_cli_lifecycle.py index b9ed635..4012353 100644 --- a/tests/integration/test_cli_lifecycle.py +++ b/tests/integration/test_cli_lifecycle.py @@ -139,25 +139,25 @@ def test_incremental_install(self, tmp_path: Path) -> None: # install second agent (incremental) result = runner.invoke( app, - ["install", "--agent", "cursor", "--config", str(config)], + ["install", "--agent", "opencode", "--config", str(config)], ) assert result.exit_code == 0 state = Installation.from_json( (installation_path(tmp_path)).read_text(encoding="utf-8") ) assert "claude-code" in state.installed_agents - assert "cursor" in state.installed_agents + assert "opencode" in state.installed_agents # status should show both agents result = runner.invoke(app, ["status", "--config", str(config)]) assert "claude-code" in result.output - assert "cursor" in result.output + assert "opencode" in result.output # update regenerates for both result = runner.invoke(app, ["update", "--config", str(config)]) assert result.exit_code == 0 assert "claude-code" in result.output - assert "cursor" in result.output + assert "opencode" in result.output def test_comma_separated_install(self, tmp_path: Path) -> None: """Install multiple agents at once with comma-separated list.""" @@ -171,7 +171,7 @@ def test_comma_separated_install(self, tmp_path: Path) -> None: [ "install", "--agent", - "claude-code,cursor,copilot", + "claude-code,opencode,copilot", "--config", str(config), ], @@ -182,7 +182,7 @@ def test_comma_separated_install(self, tmp_path: Path) -> None: ) assert len(state.installed_agents) == 3 assert "claude-code" in state.installed_agents - assert "cursor" in state.installed_agents + assert "opencode" in state.installed_agents assert "copilot" in state.installed_agents diff --git a/tests/integration/test_hook_venv_fallback.py b/tests/integration/test_hook_venv_fallback.py index f29dc8e..1fb98e9 100644 --- a/tests/integration/test_hook_venv_fallback.py +++ b/tests/integration/test_hook_venv_fallback.py @@ -126,44 +126,6 @@ def test_pre_commit_runs_with_minimal_path(self, tmp_path: Path) -> None: ) -class TestCursorHookBakesPython: - """Cursor hook embeds an absolute python interpreter path.""" - - def test_check_sh_uses_absolute_python(self, tmp_path: Path) -> None: - _scaffold_project(tmp_path, agent="cursor") - hook_path = tmp_path / ".cursor" / "hooks" / "check.sh" - assert hook_path.is_file() - content = hook_path.read_text(encoding="utf-8") - # Pre-fix shape: `... | python3 -m ac_guard.action_guard ...`. - # Post-fix shape: `... | "" -m ac_guard.action_guard ...`. - assert "-m ac_guard.action_guard" in content - assert "| python3 -m" not in content - baked = _baked_path(content, r'\| "([^"]+)" -m ac_guard\.action_guard') - assert os.path.isabs(baked), f"baked python path is not absolute: {baked!r}" - - @_skip_on_windows - def test_check_sh_runs_with_minimal_path(self, tmp_path: Path) -> None: - """Cursor hook fires under a stripped PATH (POSIX-only).""" - _scaffold_project(tmp_path, agent="cursor") - hook_path = tmp_path / ".cursor" / "hooks" / "check.sh" - - # Cursor hook reads stdin JSON and prints a JSON decision. - result = subprocess.run( - [str(hook_path)], - input='{"tool":"Read","args":{}}', - cwd=tmp_path, - env={"PATH": _MINIMAL_PATH, "HOME": str(tmp_path)}, - capture_output=True, - text=True, - check=False, - ) - # 127 = "command not found" — would mean the bake failed and - # the hook fell back to PATH lookup of `python3`. - assert result.returncode != 127, ( - f"cursor hook fell back to PATH lookup; stderr={result.stderr!r}" - ) - - class TestOpenCodePluginBakesPython: """OpenCode TS plugin source embeds an absolute python path constant.""" diff --git a/tests/unit/test_action_guard/test_cli.py b/tests/unit/test_action_guard/test_cli.py index 1881e04..9d21fae 100644 --- a/tests/unit/test_action_guard/test_cli.py +++ b/tests/unit/test_action_guard/test_cli.py @@ -115,7 +115,7 @@ def test_agent_from_stdin_is_recorded(self, tmp_path: Path) -> None: { "tool_name": "Write", "tool_input": {"file_path": ".git/config"}, - "agent": "cursor", + "agent": "opencode", }, tmp_path, ) @@ -124,4 +124,4 @@ def test_agent_from_stdin_is_recorded(self, tmp_path: Path) -> None: record = json.loads( audit_path.read_text(encoding="utf-8").strip().split("\n")[0] ) - assert record["agent"] == "cursor" + assert record["agent"] == "opencode" diff --git a/tests/unit/test_action_guard/test_core.py b/tests/unit/test_action_guard/test_core.py index dfbb6b6..d0d37ed 100644 --- a/tests/unit/test_action_guard/test_core.py +++ b/tests/unit/test_action_guard/test_core.py @@ -470,9 +470,9 @@ def test_unknown_tool_skips_audit(self, tmp_path: Path) -> None: def test_passes_agent_to_record(self, tmp_path: Path) -> None: _write_policy(tmp_path, _policy_with_audit(enabled=True)) - evaluate("Write", {"file_path": "guard.yaml"}, tmp_path, agent="cursor") + evaluate("Write", {"file_path": "guard.yaml"}, tmp_path, agent="opencode") audit_path = tmp_path / ".ac-guard" / "audit.jsonl" record = json.loads( audit_path.read_text(encoding="utf-8").strip().split("\n")[0] ) - assert record["agent"] == "cursor" + assert record["agent"] == "opencode" diff --git a/tests/unit/test_adapters/_snapshots/cursor__hook__.cursor__hooks__check.sh b/tests/unit/test_adapters/_snapshots/cursor__hook__.cursor__hooks__check.sh deleted file mode 100644 index 28393bf..0000000 --- a/tests/unit/test_adapters/_snapshots/cursor__hook__.cursor__hooks__check.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash -# Cursor Hook for AI Code Guard -# -# Generated by `ac-guard install --agent cursor`. -# This file is managed by AI Code Guard — do not edit manually. -# -# Cursor hook format: -# - Input: stdin JSON (tool/command + args) -# - Output: stdout JSON {"permission": "allow/deny"} -# - Exit code: 0 (non-zero may cause Cursor issues) - -set -e - -# Read input from stdin (Cursor format varies by hook type) -input=$(cat) - -# Parse tool call (Cursor uses different formats for different hooks) -tool_name=$(echo "$input" | jq -r '.tool // .command // .name // "unknown"' 2>/dev/null || echo "unknown") - -# Build Action guard input JSON (agent identifies the caller in audit records) -action_guard_input=$(jq -n \ - --arg tn "$tool_name" \ - --argjson ti "$(echo "$input" | jq '.args // .arguments // {}' 2>/dev/null || echo '{}')" \ - '{tool_name: $tn, tool_input: $ti, agent: "cursor"}') - -# Call Action guard via Python subprocess. -# python_executable is baked at install time so the hook works even when -# Cursor launches it from a shell whose $PATH lacks the project's venv. -action_guard_output=$(echo "$action_guard_input" | "/usr/local/bin/python3" -m ac_guard.action_guard 2>/dev/null || echo '{"decision": "deny"}') - -# Extract decision -decision=$(echo "$action_guard_output" | jq -r '.decision // "deny"' 2>/dev/null || echo "deny") - -# Cursor does not support "ask" — downgrade to "deny" -if [ "$decision" = "ask" ]; then - decision="deny" -fi - -# Output Cursor expected format -echo "{\"permission\": \"$decision\"}" diff --git a/tests/unit/test_adapters/_snapshots/cursor__hook_manifest.txt b/tests/unit/test_adapters/_snapshots/cursor__hook_manifest.txt deleted file mode 100644 index 704c522..0000000 --- a/tests/unit/test_adapters/_snapshots/cursor__hook_manifest.txt +++ /dev/null @@ -1 +0,0 @@ -.cursor/hooks/check.sh executable=True diff --git a/tests/unit/test_adapters/_snapshots/cursor__rule_doc.md b/tests/unit/test_adapters/_snapshots/cursor__rule_doc.md deleted file mode 100644 index 9466858..0000000 --- a/tests/unit/test_adapters/_snapshots/cursor__rule_doc.md +++ /dev/null @@ -1,48 +0,0 @@ - - ---- -globs: **/* ---- - -## Behavior Constraints - -The following rules define what operations are permitted, blocked, or require approval. - -### Read Operations - -**Forbidden** (blocked): -- `file:.env*` — environment files contain secrets - -> **Note**: This Agent does not support user confirmation prompts. -> Require-approval rules are treated as forbidden. - -**Require Approval** → **Blocked** (no ask capability): -- `file:**/credentials.json` — contains credentials, confirm before reading - -**Allowed** (permitted): -- `file:**/*.py` - -### Write Operations - -**Forbidden** (blocked): -- `file:.git/**` — repo internals must not be edited - -> **Note**: This Agent does not support user confirmation prompts. -> Require-approval rules are treated as forbidden. - -**Require Approval** → **Blocked** (no ask capability): -- `file:pyproject.toml` — dependency change — confirm intent - -### Execute Operations - -**Forbidden** (blocked): -- `^rm\s+-rf\s+/` — filesystem destruction - -> **Note**: This Agent does not support user confirmation prompts. -> Require-approval rules are treated as forbidden. - -**Require Approval** → **Blocked** (no ask capability): -- `shell:git push --force*` — rewrites remote history - -**Allowed** (permitted): -- `shell:pytest *` diff --git a/tests/unit/test_adapters/test__render.py b/tests/unit/test_adapters/test__render.py index 60c0bdf..0dcbda5 100644 --- a/tests/unit/test_adapters/test__render.py +++ b/tests/unit/test_adapters/test__render.py @@ -5,14 +5,12 @@ from ac_guard.adapters._render import _TEMPLATE_DIR, render_hook, render_rule_doc from ac_guard.adapters.builtins.claude_code import ClaudeCodeAdapter from ac_guard.adapters.builtins.copilot import CopilotAdapter -from ac_guard.adapters.builtins.cursor import CursorAdapter from ac_guard.adapters.builtins.kilocode import KiloCodeAdapter from ac_guard.adapters.builtins.opencode import OpenCodeAdapter from ac_guard.config.models import BehaviorConfig, OperationRules, Rule from ac_guard.domain import managed_block _CLAUDE_CODE = ClaudeCodeAdapter() -_CURSOR = CursorAdapter() _OPENCODE = OpenCodeAdapter() _COPILOT = CopilotAdapter() _KILOCODE = KiloCodeAdapter() @@ -27,18 +25,6 @@ def test_claude_code_template_exists(self) -> None: assert isinstance(result, str) assert len(result) > 0 - def test_cursor_template_exists(self) -> None: - behavior = BehaviorConfig.empty() - result = render_rule_doc(_CURSOR, behavior) - assert isinstance(result, str) - assert len(result) > 0 - - def test_cursor_has_frontmatter(self) -> None: - behavior = BehaviorConfig.empty() - result = render_rule_doc(_CURSOR, behavior) - assert "---" in result - assert "globs:" in result - def test_opencode_template_exists(self) -> None: behavior = BehaviorConfig.empty() result = render_rule_doc(_OPENCODE, behavior) @@ -89,13 +75,6 @@ def test_claude_code_hook_exists(self) -> None: assert len(result) > 0 assert "import json" in result # Python script - def test_cursor_hook_exists(self) -> None: - behavior = BehaviorConfig.empty() - result = render_hook(_CURSOR, behavior) - assert isinstance(result, str) - assert len(result) > 0 - assert "#!/bin/bash" in result # Shell script - def test_opencode_hook_exists(self) -> None: behavior = BehaviorConfig.empty() result = render_hook(_OPENCODE, behavior) @@ -109,12 +88,6 @@ def test_claude_code_hook_is_python(self) -> None: assert "def main()" in result assert "sys.stdin" in result - def test_cursor_hook_is_shell(self) -> None: - behavior = BehaviorConfig.empty() - result = render_hook(_CURSOR, behavior) - assert "input=$(cat)" in result - assert "jq" in result - def test_opencode_hook_is_typescript(self) -> None: behavior = BehaviorConfig.empty() result = render_hook(_OPENCODE, behavior) @@ -130,7 +103,7 @@ class TestTemplateStems: """ def test_rule_doc_template_exists_for_each_builtin(self) -> None: - adapters = [_CLAUDE_CODE, _CURSOR, _OPENCODE, _COPILOT, _KILOCODE] + adapters = [_CLAUDE_CODE, _OPENCODE, _COPILOT, _KILOCODE] rule_doc_dir = _TEMPLATE_DIR / "rule_docs" for adapter in adapters: template = rule_doc_dir / f"{adapter._template_stem}.md.j2" @@ -140,7 +113,7 @@ def test_rule_doc_template_exists_for_each_builtin(self) -> None: def test_hook_template_exists_for_each_block_capable_builtin(self) -> None: # Only adapters with can_block=True render hook scripts. - block_capable = [_CLAUDE_CODE, _CURSOR, _OPENCODE] + block_capable = [_CLAUDE_CODE, _OPENCODE] hook_dir = _TEMPLATE_DIR / "hooks" for adapter in block_capable: template = hook_dir / f"{adapter._template_stem}.j2" diff --git a/tests/unit/test_adapters/test_builtins/test_cursor.py b/tests/unit/test_adapters/test_builtins/test_cursor.py deleted file mode 100644 index d2c349b..0000000 --- a/tests/unit/test_adapters/test_builtins/test_cursor.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Tests for ac_guard.adapters.builtins.cursor — Cursor Agent adapter.""" - -from __future__ import annotations - -from ac_guard.adapters.builtins.cursor import CursorAdapter -from ac_guard.config.models import BehaviorConfig, OperationRules, Rule -from ac_guard.domain import managed_block - -# --------------------------------------------------------------------------- -# Adapter Properties -# --------------------------------------------------------------------------- - - -class TestCursorAdapterProperties: - def test_name(self) -> None: - adapter = CursorAdapter() - assert adapter.name == "cursor" - - def test_capabilities_can_block_true(self) -> None: - adapter = CursorAdapter() - caps = adapter.capabilities - assert caps.can_block is True - - def test_capabilities_can_ask_false(self) -> None: - # Cursor has limited ask capability, treated as False - adapter = CursorAdapter() - caps = adapter.capabilities - assert caps.can_ask is False - - def test_rule_doc_path(self) -> None: - adapter = CursorAdapter() - # Cursor uses .cursor/rules/ directory with .mdc extension - assert ".cursor/rules/" in adapter.rule_doc_path() - assert adapter.rule_doc_path().endswith(".mdc") - - -# --------------------------------------------------------------------------- -# Rule Document Rendering -# --------------------------------------------------------------------------- - - -class TestCursorAdapterRenderRuleDoc: - def test_output_is_raw_content_without_markers(self) -> None: - """Adapter returns plain .mdc content; writer layer adds markers.""" - adapter = CursorAdapter() - behavior = BehaviorConfig.empty() - result = adapter.render_rule_doc(behavior) - assert not managed_block.has(result, path=adapter.rule_doc_path()) - - def test_output_structure_with_rules(self) -> None: - adapter = CursorAdapter() - behavior = BehaviorConfig( - read=OperationRules.empty(), - write=OperationRules( - forbidden=[Rule(pattern="file:.git/**", reason="git internal")], - require_approval=[], - allow=[Rule(pattern="file:src/**")], - ), - execute=OperationRules.empty(), - ) - result = adapter.render_rule_doc(behavior) - assert "Write" in result or "write" in result.lower() - - def test_empty_behavior_produces_valid_output(self) -> None: - adapter = CursorAdapter() - behavior = BehaviorConfig.empty() - result = adapter.render_rule_doc(behavior) - assert len(result) > 0 - assert not managed_block.has(result, path=adapter.rule_doc_path()) - - -# --------------------------------------------------------------------------- -# Hook Files -# --------------------------------------------------------------------------- - - -class TestCursorAdapterHookFiles: - def test_returns_hook_files(self) -> None: - adapter = CursorAdapter() - behavior = BehaviorConfig.empty() - files = adapter.hook_files(behavior) - assert len(files) > 0 - - def test_hook_file_paths(self) -> None: - adapter = CursorAdapter() - behavior = BehaviorConfig.empty() - files = adapter.hook_files(behavior) - paths = [f.path for f in files] - # Cursor hooks under .cursor/hooks/ - assert any(".cursor/hooks/" in p for p in paths) - - def test_hook_files_have_content(self) -> None: - adapter = CursorAdapter() - behavior = BehaviorConfig.empty() - files = adapter.hook_files(behavior) - for f in files: - assert len(f.content) > 0 diff --git a/tests/unit/test_adapters/test_hooks.py b/tests/unit/test_adapters/test_hooks.py index 43bec88..ac751dc 100644 --- a/tests/unit/test_adapters/test_hooks.py +++ b/tests/unit/test_adapters/test_hooks.py @@ -9,14 +9,12 @@ from ac_guard.adapters._render import render_hook from ac_guard.adapters.builtins.claude_code import ClaudeCodeAdapter -from ac_guard.adapters.builtins.cursor import CursorAdapter from ac_guard.adapters.builtins.opencode import OpenCodeAdapter from ac_guard.config.models import BehaviorConfig # Adapter instances are stateless and cheap; reuse module-level # singletons rather than constructing per-test. _CLAUDE_CODE = ClaudeCodeAdapter() -_CURSOR = CursorAdapter() _OPENCODE = OpenCodeAdapter() @@ -74,37 +72,6 @@ def test_generated_hook_is_valid_python(self) -> None: compile(content, "", "exec") -class TestCursorHook: - """Tests for Cursor hook template.""" - - def test_renders_bash_script(self) -> None: - """Cursor hook renders a bash script.""" - content = render_hook(_CURSOR, BehaviorConfig.empty()) - assert content.startswith("#!/bin/bash") - - def test_calls_action_guard_subprocess(self) -> None: - """Cursor hook calls action_guard via the baked python interpreter.""" - content = render_hook(_CURSOR, BehaviorConfig.empty()) - assert "-m ac_guard.action_guard" in content - # Bake-time injection: must use an absolute path, not bare `python3`. - assert "| python3 -m" not in content - match = re.search(r'\| "([^"]+)" -m ac_guard\.action_guard', content) - assert match is not None, "expected baked python path in cursor hook" - assert os.path.isabs(match.group(1)), ( - f"baked python path is not absolute: {match.group(1)!r}" - ) - - def test_outputs_permission_format(self) -> None: - """Cursor hook outputs permission JSON.""" - content = render_hook(_CURSOR, BehaviorConfig.empty()) - assert '"permission"' in content - - def test_downgrades_ask_to_deny(self) -> None: - """Cursor hook downgrades ask to deny (no ask support).""" - content = render_hook(_CURSOR, BehaviorConfig.empty()) - assert 'decision" = "ask"' in content or "ask" in content - - class TestOpenCodeHook: """Tests for OpenCode hook template.""" diff --git a/tests/unit/test_adapters/test_registry.py b/tests/unit/test_adapters/test_registry.py index 5212663..89466e0 100644 --- a/tests/unit/test_adapters/test_registry.py +++ b/tests/unit/test_adapters/test_registry.py @@ -13,7 +13,7 @@ # Expected closed set of built-in adapters; cross-checked against the # snapshot suite (test_snapshots.py) that exercises render output. -_BUILTIN_NAMES = ("claude-code", "cursor", "opencode", "copilot", "kilocode") +_BUILTIN_NAMES = ("claude-code", "opencode", "copilot", "kilocode") # --------------------------------------------------------------------------- @@ -37,8 +37,8 @@ def test_unknown_name_raises_not_found(self) -> None: def test_returns_same_instance_each_call(self) -> None: # The registry holds singletons; lookup must not construct a # fresh adapter on each call. - first = get_adapter("cursor") - second = get_adapter("cursor") + first = get_adapter("claude-code") + second = get_adapter("claude-code") assert first is second diff --git a/tests/unit/test_adapters/test_snapshots.py b/tests/unit/test_adapters/test_snapshots.py index dabfb31..0111079 100644 --- a/tests/unit/test_adapters/test_snapshots.py +++ b/tests/unit/test_adapters/test_snapshots.py @@ -25,7 +25,7 @@ _SNAPSHOT_DIR = Path(__file__).parent / "_snapshots" _UPDATE = os.environ.get("AC_GUARD_UPDATE_SNAPSHOTS") == "1" -_BUILTIN_NAMES = ("claude-code", "cursor", "opencode", "copilot", "kilocode") +_BUILTIN_NAMES = ("claude-code", "opencode", "copilot", "kilocode") # Hook templates bake ``sys.executable`` so the rendered hook can # re-exec into the interpreter that ran ``ac-guard install``. That diff --git a/tests/unit/test_cli/test_install.py b/tests/unit/test_cli/test_install.py index 6ead536..6f79763 100644 --- a/tests/unit/test_cli/test_install.py +++ b/tests/unit/test_cli/test_install.py @@ -77,7 +77,6 @@ def test_install_no_agent_lists_available(self, tmp_path: Path) -> None: ) assert result.exit_code == 0 assert "claude-code" in result.output - assert "cursor" in result.output def test_install_single_agent_creates_artifacts( self, project_with_config: Path @@ -102,26 +101,26 @@ def test_install_multiple_agents(self, project_with_config: Path) -> None: config = project_with_config / "guard.yaml" result = runner.invoke( app, - ["install", "--agent", "claude-code,cursor", "--config", str(config)], + ["install", "--agent", "claude-code,opencode", "--config", str(config)], ) assert result.exit_code == 0 state_path = installation_path(project_with_config) state = Installation.from_json(state_path.read_text(encoding="utf-8")) assert "claude-code" in state.installed_agents - assert "cursor" in state.installed_agents + assert "opencode" in state.installed_agents def test_install_incremental_appends_agents(self, installed_project: Path) -> None: """Second install appends new agent to existing agents.""" config = installed_project / "guard.yaml" result = runner.invoke( app, - ["install", "--agent", "cursor", "--config", str(config)], + ["install", "--agent", "opencode", "--config", str(config)], ) assert result.exit_code == 0 state_path = installation_path(installed_project) state = Installation.from_json(state_path.read_text(encoding="utf-8")) assert "claude-code" in state.installed_agents - assert "cursor" in state.installed_agents + assert "opencode" in state.installed_agents def test_install_duplicate_agent_is_idempotent( self, installed_project: Path diff --git a/tests/unit/test_cli/test_status.py b/tests/unit/test_cli/test_status.py index 93af9df..3c75648 100644 --- a/tests/unit/test_cli/test_status.py +++ b/tests/unit/test_cli/test_status.py @@ -484,7 +484,6 @@ def test_agents_lists_all(self) -> None: result = runner.invoke(app, ["agents"]) assert result.exit_code == 0 assert "claude-code" in result.output.lower() - assert "cursor" in result.output.lower() assert "opencode" in result.output.lower() assert "copilot" in result.output.lower() assert "kilocode" in result.output.lower() diff --git a/tests/unit/test_generator/test_core.py b/tests/unit/test_generator/test_core.py index b8dff11..a5d67c2 100644 --- a/tests/unit/test_generator/test_core.py +++ b/tests/unit/test_generator/test_core.py @@ -115,17 +115,17 @@ def test_creates_ac_guard_directory(self, tmp_path: Path) -> None: def test_writes_valid_json(self, tmp_path: Path) -> None: state = Installation( ac_guard_version="0.1.0", - installed_agents=["claude-code", "cursor"], + installed_agents=["claude-code", "opencode"], config_hash="abc123", installed_at=datetime(2026, 4, 16, 14, 0, 0), - artifacts=["CLAUDE.md", ".cursor/rules"], + artifacts=["CLAUDE.md", "AGENTS.md"], ) write_installation(tmp_path, state) content = (installation_path(tmp_path)).read_text(encoding="utf-8") data = json.loads(content) assert data["ac_guard_version"] == "0.1.0" - assert data["installed_agents"] == ["claude-code", "cursor"] + assert data["installed_agents"] == ["claude-code", "opencode"] def test_overwrites_existing_state(self, tmp_path: Path) -> None: # Write initial state @@ -139,14 +139,14 @@ def test_overwrites_existing_state(self, tmp_path: Path) -> None: # Write updated state state2 = Installation( ac_guard_version="0.1.0", - installed_agents=["claude-code", "cursor"], + installed_agents=["claude-code", "opencode"], installed_at=datetime.now(), ) write_installation(tmp_path, state2) result = read_installation(tmp_path) assert result is not None - assert result.installed_agents == ["claude-code", "cursor"] + assert result.installed_agents == ["claude-code", "opencode"] class TestCreateInstallation: @@ -330,13 +330,13 @@ def test_update_preserves_single_marker_pair(self, tmp_path: Path) -> None: assert "v2" in content assert "v1" not in content - def test_install_wraps_mdc_rule_docs_too(self, tmp_path: Path) -> None: - """Cursor uses .mdc; it must be wrapped like .md.""" + def test_install_wraps_non_md_rule_docs_too(self, tmp_path: Path) -> None: + """Non-.md rule docs must be wrapped like .md.""" write_artifacts( tmp_path, - [FileSpec(path=".cursor/rules/behavior.mdc", content="raw")], + [FileSpec(path=".github/copilot-instructions.md", content="raw")], ) - content = (tmp_path / ".cursor" / "rules" / "behavior.mdc").read_text( + content = (tmp_path / ".github" / "copilot-instructions.md").read_text( encoding="utf-8" ) assert self._count(content, MARKER_BEGIN) == 1 diff --git a/tests/unit/test_generator/test_models.py b/tests/unit/test_generator/test_models.py index 82c25d8..0570553 100644 --- a/tests/unit/test_generator/test_models.py +++ b/tests/unit/test_generator/test_models.py @@ -40,14 +40,14 @@ def test_to_dict(self) -> None: now = datetime(2026, 4, 16, 12, 0, 0) inst = Installation( ac_guard_version="0.1.0", - installed_agents=["claude-code", "cursor"], + installed_agents=["claude-code", "opencode"], config_hash="abc123", installed_at=now, artifacts=["CLAUDE.md", ".claude/settings.json"], ) d = inst.to_dict() assert d["ac_guard_version"] == "0.1.0" - assert d["installed_agents"] == ["claude-code", "cursor"] + assert d["installed_agents"] == ["claude-code", "opencode"] assert d["config_hash"] == "abc123" assert d["installed_at"] == "2026-04-16T12:00:00" assert d["artifacts"] == ["CLAUDE.md", ".claude/settings.json"] @@ -67,14 +67,14 @@ def test_to_json(self) -> None: def test_from_dict(self) -> None: d = { "ac_guard_version": "0.2.0", - "installed_agents": ["cursor"], + "installed_agents": ["opencode"], "config_hash": "def456", "installed_at": "2026-04-15T10:30:00", "artifacts": ["file1", "file2"], } inst = Installation.from_dict(d) assert inst.ac_guard_version == "0.2.0" - assert inst.installed_agents == ["cursor"] + assert inst.installed_agents == ["opencode"] assert inst.config_hash == "def456" assert inst.installed_at == datetime(2026, 4, 15, 10, 30, 0) assert inst.artifacts == ["file1", "file2"] @@ -89,10 +89,10 @@ def test_from_dict_missing_fields(self) -> None: def test_json_roundtrip(self) -> None: original = Installation( ac_guard_version="0.1.0", - installed_agents=["claude-code", "cursor"], + installed_agents=["claude-code", "opencode"], config_hash="abc123", installed_at=datetime(2026, 4, 16, 14, 30, 0), - artifacts=["CLAUDE.md", ".cursor/rules/behavior.mdc"], + artifacts=["CLAUDE.md", "AGENTS.md"], ) json_str = original.to_json() restored = Installation.from_json(json_str)