diff --git a/docs/en/agent-integrations/08-community-plugins.md b/docs/en/agent-integrations/08-community-plugins.md index 2d5e90053f..901e032ebf 100644 --- a/docs/en/agent-integrations/08-community-plugins.md +++ b/docs/en/agent-integrations/08-community-plugins.md @@ -62,7 +62,7 @@ For development, debugging, or PR testing, install the plugin from this reposito git clone https://github.com/volcengine/OpenViking.git cd OpenViking mkdir -p ~/.config/opencode/plugins/openviking -cp examples/opencode-plugin/wrappers/openviking.mjs ~/.config/opencode/plugins/openviking.mjs +cp examples/opencode-plugin/wrappers/openviking.js ~/.config/opencode/plugins/openviking.js cp examples/opencode-plugin/index.mjs examples/opencode-plugin/package.json ~/.config/opencode/plugins/openviking/ cp -r examples/opencode-plugin/lib ~/.config/opencode/plugins/openviking/ cd ~/.config/opencode/plugins/openviking @@ -73,7 +73,7 @@ This source install creates the layout OpenCode can discover: ```text ~/.config/opencode/plugins/ -├── openviking.mjs +├── openviking.js └── openviking/ ├── index.mjs ├── package.json @@ -81,7 +81,8 @@ This source install creates the layout OpenCode can discover: └── node_modules/ ``` -The top-level `openviking.mjs` is only a wrapper that forwards OpenCode's first-level plugin entry to the installed package directory. +The top-level `openviking.js` is only a wrapper that forwards OpenCode's first-level plugin entry to the installed package directory. +Use the `.js` wrapper for source installs; OpenCode's local plugin scanner discovers JavaScript/TypeScript plugin files. ### Configure @@ -139,7 +140,7 @@ Ask OpenCode to search or browse OpenViking memory, or request a manual session | Issue | What to check | |-------|---------------| -| Plugin does not load | Confirm `~/.config/opencode/opencode.json` references `openviking-opencode-plugin`, or that `~/.config/opencode/plugins/openviking.mjs` exists for source installs | +| Plugin does not load | Confirm `~/.config/opencode/opencode.json` references `openviking-opencode-plugin`, or that `~/.config/opencode/plugins/openviking.js` exists for source installs | | Tools call the wrong server | Check `endpoint` in `~/.config/opencode/openviking-config.json`, or set `OPENVIKING_PLUGIN_CONFIG` to the intended config path | | 401 / 403 from OpenViking | Verify `OPENVIKING_API_KEY`; for trusted-mode deployments, also verify `OPENVIKING_ACCOUNT` and `OPENVIKING_USER` | | Recall is empty | Confirm the OpenViking server has indexed memories/resources and that `autoRecall.enabled` is `true` | diff --git a/docs/images/agents/en/opencode.md b/docs/images/agents/en/opencode.md index 15f640422d..2e32f2d99f 100644 --- a/docs/images/agents/en/opencode.md +++ b/docs/images/agents/en/opencode.md @@ -38,7 +38,7 @@ For development, debugging, or PR testing, copy the plugin from the OpenViking r git clone https://github.com/volcengine/OpenViking.git cd OpenViking mkdir -p ~/.config/opencode/plugins/openviking -cp examples/opencode-plugin/wrappers/openviking.mjs ~/.config/opencode/plugins/openviking.mjs +cp examples/opencode-plugin/wrappers/openviking.js ~/.config/opencode/plugins/openviking.js cp examples/opencode-plugin/index.mjs examples/opencode-plugin/package.json ~/.config/opencode/plugins/openviking/ cp -r examples/opencode-plugin/lib ~/.config/opencode/plugins/openviking/ cd ~/.config/opencode/plugins/openviking @@ -49,7 +49,7 @@ The source install creates: ```text ~/.config/opencode/plugins/ -├── openviking.mjs +├── openviking.js └── openviking/ ├── index.mjs ├── package.json @@ -108,7 +108,7 @@ Ask OpenCode to browse OpenViking or commit the current session. Check runtime l | Symptom | Fix | |---------|-----| -| Plugin is not loaded | Check `~/.config/opencode/opencode.json` for package installs, or `~/.config/opencode/plugins/openviking.mjs` for source installs | +| Plugin is not loaded | Check `~/.config/opencode/opencode.json` for package installs, or `~/.config/opencode/plugins/openviking.js` for source installs | | Tools call the wrong server | Check `endpoint`, or set `OPENVIKING_PLUGIN_CONFIG` to the intended config path | | 401 / 403 from OpenViking | Verify `OPENVIKING_API_KEY`; trusted-mode deployments also need `OPENVIKING_ACCOUNT` and `OPENVIKING_USER` | | Recall is empty | Confirm OpenViking has memories/resources and `autoRecall.enabled` is `true` | diff --git a/docs/images/agents/zh/opencode.md b/docs/images/agents/zh/opencode.md index 8c525cc262..6da179ba6d 100644 --- a/docs/images/agents/zh/opencode.md +++ b/docs/images/agents/zh/opencode.md @@ -39,7 +39,7 @@ curl http://localhost:1933/health git clone https://github.com/volcengine/OpenViking.git cd OpenViking mkdir -p ~/.config/opencode/plugins/openviking -cp examples/opencode-plugin/wrappers/openviking.mjs ~/.config/opencode/plugins/openviking.mjs +cp examples/opencode-plugin/wrappers/openviking.js ~/.config/opencode/plugins/openviking.js cp examples/opencode-plugin/index.mjs examples/opencode-plugin/package.json ~/.config/opencode/plugins/openviking/ cp -r examples/opencode-plugin/lib ~/.config/opencode/plugins/openviking/ cd ~/.config/opencode/plugins/openviking @@ -50,7 +50,7 @@ npm install ```text ~/.config/opencode/plugins/ -├── openviking.mjs +├── openviking.js └── openviking/ ├── index.mjs ├── package.json @@ -109,7 +109,7 @@ export OPENVIKING_PEER_ID="opencode" # 可选,peer 维度记忆路由需要 | 现象 | 修复 | |------|------| -| 插件没有加载 | package 安装检查 `~/.config/opencode/opencode.json`;源码安装检查 `~/.config/opencode/plugins/openviking.mjs` | +| 插件没有加载 | package 安装检查 `~/.config/opencode/opencode.json`;源码安装检查 `~/.config/opencode/plugins/openviking.js` | | Tools 连到了错误的 server | 检查 `endpoint`,或用 `OPENVIKING_PLUGIN_CONFIG` 指向正确配置文件 | | OpenViking 返回 401 / 403 | 检查 `OPENVIKING_API_KEY`;trusted-mode 部署还需要 `OPENVIKING_ACCOUNT` 和 `OPENVIKING_USER` | | recall 为空 | 确认 OpenViking 中已有 memories/resources,并且 `autoRecall.enabled` 为 `true` | diff --git a/docs/zh/agent-integrations/08-community-plugins.md b/docs/zh/agent-integrations/08-community-plugins.md index 4db30d53ae..1f678a21ac 100644 --- a/docs/zh/agent-integrations/08-community-plugins.md +++ b/docs/zh/agent-integrations/08-community-plugins.md @@ -62,7 +62,7 @@ curl http://localhost:1933/health git clone https://github.com/volcengine/OpenViking.git cd OpenViking mkdir -p ~/.config/opencode/plugins/openviking -cp examples/opencode-plugin/wrappers/openviking.mjs ~/.config/opencode/plugins/openviking.mjs +cp examples/opencode-plugin/wrappers/openviking.js ~/.config/opencode/plugins/openviking.js cp examples/opencode-plugin/index.mjs examples/opencode-plugin/package.json ~/.config/opencode/plugins/openviking/ cp -r examples/opencode-plugin/lib ~/.config/opencode/plugins/openviking/ cd ~/.config/opencode/plugins/openviking @@ -73,7 +73,7 @@ npm install ```text ~/.config/opencode/plugins/ -├── openviking.mjs +├── openviking.js └── openviking/ ├── index.mjs ├── package.json @@ -81,7 +81,8 @@ npm install └── node_modules/ ``` -顶层 `openviking.mjs` 只是一个 wrapper,用来把 OpenCode 可发现的一级插件入口转发到实际安装目录。 +顶层 `openviking.js` 只是一个 wrapper,用来把 OpenCode 可发现的一级插件入口转发到实际安装目录。 +源码安装请使用 `.js` wrapper;OpenCode 的本地插件扫描器会发现 JavaScript/TypeScript 插件文件。 ### 配置 @@ -139,7 +140,7 @@ export OPENVIKING_PEER_ID="opencode" # 可选,peer 维度记忆路由需要 | 问题 | 排查方向 | |------|----------| -| 插件没有加载 | 确认 `~/.config/opencode/opencode.json` 引用了 `openviking-opencode-plugin`;源码安装时确认 `~/.config/opencode/plugins/openviking.mjs` 存在 | +| 插件没有加载 | 确认 `~/.config/opencode/opencode.json` 引用了 `openviking-opencode-plugin`;源码安装时确认 `~/.config/opencode/plugins/openviking.js` 存在 | | Tools 连到了错误的 server | 检查 `~/.config/opencode/openviking-config.json` 里的 `endpoint`,或用 `OPENVIKING_PLUGIN_CONFIG` 指向正确配置文件 | | OpenViking 返回 401 / 403 | 检查 `OPENVIKING_API_KEY`;trusted-mode 部署还要检查 `OPENVIKING_ACCOUNT` 和 `OPENVIKING_USER` | | recall 为空 | 确认 OpenViking server 中已有 memories/resources,并且 `autoRecall.enabled` 为 `true` | diff --git a/examples/opencode-plugin/INSTALL-ZH.md b/examples/opencode-plugin/INSTALL-ZH.md index a760d27992..5f33c6ab02 100644 --- a/examples/opencode-plugin/INSTALL-ZH.md +++ b/examples/opencode-plugin/INSTALL-ZH.md @@ -50,7 +50,7 @@ curl http://localhost:1933/health ```bash mkdir -p ~/.config/opencode/plugins/openviking -cp examples/opencode-plugin/wrappers/openviking.mjs ~/.config/opencode/plugins/openviking.mjs +cp examples/opencode-plugin/wrappers/openviking.js ~/.config/opencode/plugins/openviking.js cp examples/opencode-plugin/index.mjs examples/opencode-plugin/package.json ~/.config/opencode/plugins/openviking/ cp -r examples/opencode-plugin/lib ~/.config/opencode/plugins/openviking/ cd ~/.config/opencode/plugins/openviking @@ -61,7 +61,7 @@ npm install ```text ~/.config/opencode/plugins/ -├── openviking.mjs +├── openviking.js └── openviking/ ├── index.mjs ├── package.json @@ -69,13 +69,14 @@ npm install └── node_modules/ ``` -顶层 `openviking.mjs` 只负责把 OpenCode 能发现的一级 `.mjs` 入口转发到插件目录: +顶层 `openviking.js` 只负责把 OpenCode 能发现的一级 `.js` 入口转发到插件目录: ```js export { OpenVikingPlugin, default } from "./openviking/index.mjs" ``` 这个 wrapper 只用于上面这种源码安装目录结构。npm 包安装会通过 `package.json` 直接加载 `index.mjs`。 +源码安装请使用 `.js` wrapper;OpenCode 的本地插件扫描器会发现 JavaScript/TypeScript 插件文件。 如果你使用 npm 包方式安装,也可以将 `examples/opencode-plugin` 作为一个普通 OpenCode 插件包使用。 @@ -216,7 +217,7 @@ memadd path="file:///home/alice/project/notes.md" reason="project notes" | 问题 | 排查方向 | |------|----------| -| 插件没有加载 | package 安装检查 `~/.config/opencode/opencode.json` 是否包含 `openviking-opencode-plugin`;源码安装检查 `~/.config/opencode/plugins/openviking.mjs` 是否存在 | +| 插件没有加载 | package 安装检查 `~/.config/opencode/opencode.json` 是否包含 `openviking-opencode-plugin`;源码安装检查 `~/.config/opencode/plugins/openviking.js` 是否存在 | | Tools 连到了错误的 server | 检查 `~/.config/opencode/openviking-config.json` 里的 `endpoint`,或用 `OPENVIKING_PLUGIN_CONFIG` 指向正确配置文件 | | OpenViking 返回 401 / 403 | 检查 `OPENVIKING_API_KEY`;trusted-mode 部署还要检查 `OPENVIKING_ACCOUNT` 和 `OPENVIKING_USER` | | recall 为空 | 确认 OpenViking 中已有 memories/resources,并且 `autoRecall.enabled` 为 `true` | diff --git a/examples/opencode-plugin/INSTALL.md b/examples/opencode-plugin/INSTALL.md index d8e11c566e..b3155d66f4 100644 --- a/examples/opencode-plugin/INSTALL.md +++ b/examples/opencode-plugin/INSTALL.md @@ -50,7 +50,7 @@ Run the following commands from the repository root: ```bash mkdir -p ~/.config/opencode/plugins/openviking -cp examples/opencode-plugin/wrappers/openviking.mjs ~/.config/opencode/plugins/openviking.mjs +cp examples/opencode-plugin/wrappers/openviking.js ~/.config/opencode/plugins/openviking.js cp examples/opencode-plugin/index.mjs examples/opencode-plugin/package.json ~/.config/opencode/plugins/openviking/ cp -r examples/opencode-plugin/lib ~/.config/opencode/plugins/openviking/ cd ~/.config/opencode/plugins/openviking @@ -61,7 +61,7 @@ After installation, the layout should look like this: ```text ~/.config/opencode/plugins/ -├── openviking.mjs +├── openviking.js └── openviking/ ├── index.mjs ├── package.json @@ -69,13 +69,14 @@ After installation, the layout should look like this: └── node_modules/ ``` -The top-level `openviking.mjs` forwards the first-level `.mjs` entry that OpenCode can discover to the actual plugin directory: +The top-level `openviking.js` forwards the first-level `.js` entry that OpenCode can discover to the actual plugin directory: ```js export { OpenVikingPlugin, default } from "./openviking/index.mjs" ``` This wrapper is only for source installs with the directory layout shown above. npm package installs load `index.mjs` directly through `package.json`. +Use the `.js` wrapper for source installs; OpenCode's local plugin scanner discovers JavaScript/TypeScript plugin files. If you install through an npm package, you can also use `examples/opencode-plugin` as a normal OpenCode plugin package. @@ -211,7 +212,7 @@ These are local runtime files and should not be committed to the repository. | Issue | What to check | |-------|---------------| -| Plugin does not load | For package installs, confirm `~/.config/opencode/opencode.json` contains `openviking-opencode-plugin`; for source installs, confirm `~/.config/opencode/plugins/openviking.mjs` exists | +| Plugin does not load | For package installs, confirm `~/.config/opencode/opencode.json` contains `openviking-opencode-plugin`; for source installs, confirm `~/.config/opencode/plugins/openviking.js` exists | | Tools call the wrong server | Check `endpoint` in `~/.config/opencode/openviking-config.json`, or set `OPENVIKING_PLUGIN_CONFIG` to the intended config path | | 401 / 403 from OpenViking | Verify `OPENVIKING_API_KEY`; for trusted-mode deployments, also verify `OPENVIKING_ACCOUNT` and `OPENVIKING_USER` | | Recall is empty | Confirm OpenViking has indexed memories/resources and `autoRecall.enabled` is `true` | diff --git a/examples/opencode-plugin/README.md b/examples/opencode-plugin/README.md index d593408a29..fd0fce1ff6 100644 --- a/examples/opencode-plugin/README.md +++ b/examples/opencode-plugin/README.md @@ -35,7 +35,7 @@ examples/opencode-plugin/ │ └── utils.mjs ├── tests/ └── wrappers/ - └── openviking.mjs + └── openviking.js ``` There is intentionally no `skills/openviking/SKILL.md`. The former skill behavior is implemented as tools. @@ -71,7 +71,7 @@ For development or PR testing, copy the package into OpenCode's plugin directory ```bash mkdir -p ~/.config/opencode/plugins/openviking -cp examples/opencode-plugin/wrappers/openviking.mjs ~/.config/opencode/plugins/openviking.mjs +cp examples/opencode-plugin/wrappers/openviking.js ~/.config/opencode/plugins/openviking.js cp examples/opencode-plugin/index.mjs examples/opencode-plugin/package.json ~/.config/opencode/plugins/openviking/ cp -r examples/opencode-plugin/lib ~/.config/opencode/plugins/openviking/ cd ~/.config/opencode/plugins/openviking @@ -82,7 +82,7 @@ This creates a stable OpenCode plugin layout: ```text ~/.config/opencode/plugins/ -├── openviking.mjs +├── openviking.js └── openviking/ ├── index.mjs ├── package.json @@ -90,13 +90,14 @@ This creates a stable OpenCode plugin layout: └── node_modules/ ``` -The top-level `openviking.mjs` is only a wrapper: +The top-level `openviking.js` is only a wrapper: ```js export { OpenVikingPlugin, default } from "./openviking/index.mjs" ``` This wrapper is only for source installs with the directory layout shown above. npm package installs load `index.mjs` directly through `package.json`. +Use the `.js` wrapper for source installs; OpenCode's local plugin scanner discovers JavaScript/TypeScript plugin files. ## Configuration diff --git a/examples/opencode-plugin/wrappers/openviking.mjs b/examples/opencode-plugin/wrappers/openviking.js similarity index 100% rename from examples/opencode-plugin/wrappers/openviking.mjs rename to examples/opencode-plugin/wrappers/openviking.js diff --git a/tests/oc2ov_test/README.md b/tests/oc2ov_test/README.md index f099e9accb..5ed6340af2 100644 --- a/tests/oc2ov_test/README.md +++ b/tests/oc2ov_test/README.md @@ -153,6 +153,43 @@ TEST_CONFIG = { ## 🧪 运行测试 +### 插件契约测试 + +`tests/plugins/` 是快速、低成本的 Agent 集成契约测试入口,用来在真正跑 +OpenCode、Hermes 或 OpenClaw E2E 前先验证安装文档、插件入口、工具注册和 +Hermes OpenViking preflight 是否仍然完整。 + +当前覆盖: + +- OpenCode source install 使用 `openviking.js` wrapper,避免本地插件扫描器跳过 `.mjs` 文件 +- `opencode-plugin` 暴露核心 memory tools、code tools、`memwrite` 和 `viking://` URI guard +- OpenCode 文档包含安装、健康检查、工具使用和本地 `read/glob/grep` 防误用说明 +- Hermes 文档明确 OpenViking 是内置 memory provider,无需安装插件 +- Hermes LoCoMo `e2e` benchmark 在正式导入/评测前会用 preflight 验证 Hermes 写入了目标 OpenViking + +推荐作为变更插件、agent 集成文档或 benchmark preflight 时的第一道 ovtest: + +```bash +python run_tests.py --type plugins -v +# 或 +./run.sh --plugins -v +``` + +插件相关变更推荐按这个顺序验证: + +1. 在 `tests/oc2ov_test` 运行 `python run_tests.py --type plugins -v`,先确认 + OpenCode / Hermes + OpenViking 的轻量契约没有漂移。 +2. 如果改了 `examples/opencode-plugin` 代码,在 `examples/opencode-plugin` 运行 + `npm run check && npm test`,验证 package 自身导出、工具定义和 guard 行为。 +3. 如果改了 OpenCode 安装文档或 wrapper,用 source install 方式把 + `wrappers/openviking.js` 放到 `.opencode/plugins/` 或 + `~/.config/opencode/plugins/`,给同级 `openviking/` 运行 `npm install`, + 启动 OpenCode 后检查 `/experimental/tool/ids` 里有 `memwrite`、`memsearch` + 和 `codeoutline`。 +4. 如果改了 Hermes 集成或 LoCoMo benchmark,带 `HERMES_URL` 和 `OPENVIKING_URL` + 运行 `--suite e2e`,确认 preflight 能在导入和评测前发现 Hermes 没有写入目标 + OpenViking 的配置错误。 + ### 推荐:使用 CLI 方式(更稳定) ```bash @@ -191,6 +228,12 @@ pytest test_pytest.py -v --html=reports/test_report.html --self-contained-html # 仅运行复杂场景测试 ./run.sh -x +# 仅运行插件契约测试(OpenCode / Hermes + OpenViking) +./run.sh --plugins + +# 或使用 unittest runner +python run_tests.py --type plugins -v + # 详细输出模式 ./run.sh -v diff --git a/tests/oc2ov_test/run.sh b/tests/oc2ov_test/run.sh index a407f67c41..f8297795ed 100755 --- a/tests/oc2ov_test/run.sh +++ b/tests/oc2ov_test/run.sh @@ -4,7 +4,7 @@ set -e # 检查是否在虚拟环境中 -if [[ "$VIRTUAL_ENV" == "" ]]; then +if [[ "$VIRTUAL_ENV" == "" && -f venv/bin/activate ]]; then echo "激活虚拟环境..." source venv/bin/activate fi @@ -24,6 +24,7 @@ if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then echo " -p, --p0 仅运行 P0 级测试" echo " -c, --crud 仅运行 CRUD 操作测试" echo " -x, --complex 仅运行复杂场景测试" + echo " -g, --plugins 仅运行 Agent 插件契约测试" echo " -v, --verbose 详细输出模式" echo " -r, --report 生成 HTML 测试报告" echo "" @@ -34,6 +35,7 @@ fi VERBOSE="" REPORT="" TEST_TYPE="" +PLUGIN_TEST="" # 解析参数 while [[ $# -gt 0 ]]; do @@ -62,6 +64,10 @@ while [[ $# -gt 0 ]]; do TEST_TYPE="tests/complex/" shift ;; + -g|--plugins) + PLUGIN_TEST="1" + shift + ;; *) echo "未知选项: $1" echo "使用 -h 查看帮助" @@ -74,7 +80,14 @@ done mkdir -p reports logs # 运行测试 -if [ -n "$TEST_TYPE" ]; then +if [ "$PLUGIN_TEST" = "1" ]; then + echo "运行 Agent 插件契约测试..." + if [ -n "$REPORT" ]; then + echo "插件契约测试使用 unittest runner,不生成 HTML 报告。" + REPORT="" + fi + python run_tests.py --type plugins $VERBOSE +elif [ -n "$TEST_TYPE" ]; then echo "运行 $TEST_TYPE 目录下的测试..." pytest $TEST_TYPE $VERBOSE $REPORT else diff --git a/tests/oc2ov_test/run_tests.py b/tests/oc2ov_test/run_tests.py index ddcfbbb1aa..486aa42504 100644 --- a/tests/oc2ov_test/run_tests.py +++ b/tests/oc2ov_test/run_tests.py @@ -7,7 +7,11 @@ import sys import unittest -from utils.logger import setup_logger +try: + from utils.logger import setup_logger +except ModuleNotFoundError: + def setup_logger(): + return None def get_test_suite(test_type: str = None): @@ -17,7 +21,9 @@ def get_test_suite(test_type: str = None): loader = unittest.TestLoader() suite = unittest.TestSuite() - if test_type == "p0": + if test_type == "plugins": + suite.addTests(loader.loadTestsFromName("tests.plugins.test_agent_plugin_contracts")) + elif test_type == "p0": suite.addTests(loader.loadTestsFromName("tests.p0.test_memory_write")) suite.addTests(loader.loadTestsFromName("tests.p0.test_memory_crud")) suite.addTests(loader.loadTestsFromName("tests.p0.test_context_engine")) @@ -28,6 +34,7 @@ def get_test_suite(test_type: str = None): suite.addTests(loader.loadTestsFromName("tests.p0.test_memory_crud")) suite.addTests(loader.loadTestsFromName("tests.p0.test_context_engine")) suite.addTests(loader.loadTestsFromName("tests.p0.test_memory_v2_full_suite")) + suite.addTests(loader.loadTestsFromName("tests.plugins.test_agent_plugin_contracts")) return suite @@ -37,9 +44,9 @@ def main(): parser.add_argument( "--type", "-t", - choices=["all", "p0", "v2"], + choices=["all", "p0", "v2", "plugins"], default="all", - help="测试类型: all(全部), p0(P0核心), v2(V2文件验证)", + help="测试类型: all(全部), p0(P0核心), v2(V2文件验证), plugins(Agent 插件契约)", ) parser.add_argument( "--test", diff --git a/tests/oc2ov_test/tests/plugins/__init__.py b/tests/oc2ov_test/tests/plugins/__init__.py new file mode 100644 index 0000000000..1a655759b5 --- /dev/null +++ b/tests/oc2ov_test/tests/plugins/__init__.py @@ -0,0 +1 @@ +"""Agent plugin contract tests.""" diff --git a/tests/oc2ov_test/tests/plugins/test_agent_plugin_contracts.py b/tests/oc2ov_test/tests/plugins/test_agent_plugin_contracts.py new file mode 100644 index 0000000000..ae2dcdc42a --- /dev/null +++ b/tests/oc2ov_test/tests/plugins/test_agent_plugin_contracts.py @@ -0,0 +1,158 @@ +""" +Agent plugin contract tests for ovtest. + +These tests are intentionally fast and mostly static. They capture the +installation and integration contracts that should hold before running heavier +live OpenCode or Hermes E2E checks. +""" + +import json +import re +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[4] + + +def read_text(relative_path: str) -> str: + return (REPO_ROOT / relative_path).read_text(encoding="utf-8") + + +class TestOpenCodePluginContract(unittest.TestCase): + def test_source_install_uses_scanned_js_wrapper(self): + wrapper = REPO_ROOT / "examples/opencode-plugin/wrappers/openviking.js" + stale_wrapper = REPO_ROOT / "examples/opencode-plugin/wrappers/openviking.mjs" + self.assertTrue(wrapper.exists(), "Source install must provide a .js wrapper OpenCode scans") + self.assertFalse(stale_wrapper.exists(), ".mjs wrapper is not part of the source install contract") + self.assertIn( + 'export { OpenVikingPlugin, default } from "./openviking/index.mjs"', + wrapper.read_text(encoding="utf-8"), + ) + + doc_paths = [ + "examples/opencode-plugin/README.md", + "examples/opencode-plugin/INSTALL.md", + "examples/opencode-plugin/INSTALL-ZH.md", + "docs/en/agent-integrations/08-community-plugins.md", + "docs/zh/agent-integrations/08-community-plugins.md", + "docs/images/agents/en/opencode.md", + "docs/images/agents/zh/opencode.md", + ] + for path in doc_paths: + text = read_text(path) + self.assertIn("openviking.js", text, path) + self.assertNotIn("openviking.mjs", text, path) + + def test_opencode_plugin_exposes_memory_tools_and_uri_guard(self): + index = read_text("examples/opencode-plugin/index.mjs") + memory_tools = read_text("examples/opencode-plugin/lib/memory-tools.mjs") + guard = read_text("examples/opencode-plugin/lib/viking-uri-guard.mjs") + package = json.loads(read_text("examples/opencode-plugin/package.json")) + + for tool_name in [ + "memsearch", + "memread", + "membrowse", + "memcommit", + "memgrep", + "memglob", + "memadd", + "memwrite", + "memremove", + "memqueue", + ]: + self.assertIn(f"{tool_name}:", memory_tools, tool_name) + + self.assertIn('"tool.execute.before": vikingUriGuard', index) + self.assertIn("createVikingUriGuard", index) + self.assertIn('endpoint: "/api/v1/content/write"', memory_tools) + self.assertIn('mode: args.mode ?? "create"', memory_tools) + self.assertIn("actorPeerId,", memory_tools) + self.assertIn("read:", guard) + self.assertIn("glob:", guard) + self.assertIn("grep:", guard) + self.assertIn("viking:// URIs are OpenViking virtual paths", guard) + self.assertIn("lib/viking-uri-guard.mjs", package["scripts"]["check"]) + + def test_opencode_docs_capture_best_practice_validation_flow(self): + install = read_text("examples/opencode-plugin/INSTALL.md") + readme = read_text("examples/opencode-plugin/README.md") + + self.assertIn("npm install", install) + self.assertIn("curl http://localhost:1933/health", install) + self.assertIn("memwrite", install) + self.assertIn("memread", install) + self.assertIn("membrowse", install) + self.assertIn("memsearch", install) + self.assertIn("OpenCode's local `read`, `glob`, or `grep` tools", install) + self.assertIn("OpenCode's local `read`, `glob`, and `grep` tools cannot read", readme) + + +class TestHermesOpenVikingContract(unittest.TestCase): + def test_hermes_docs_describe_native_openviking_provider(self): + doc_pairs = [ + ("docs/en/agent-integrations/05-hermes.md", ["built in", "No plugin to install"]), + ("docs/zh/agent-integrations/05-hermes.md", ["内置 OpenViking", "无需安装插件"]), + ("docs/images/agents/en/hermes.md", ["built-in memory provider", "No plugin installation"]), + ("docs/images/agents/zh/hermes.md", ["内置 OpenViking", "无需安装插件"]), + ] + for path, expected_phrases in doc_pairs: + text = read_text(path) + for phrase in expected_phrases: + self.assertIn(phrase, text, f"{path} should mention {phrase!r}") + self.assertIn("hermes memory setup", text) + self.assertIn("hermes memory status", text) + + def test_hermes_benchmark_preflight_verifies_openviking_target(self): + runner = read_text("benchmark/locomo/hermes/run_full_eval.sh") + + self.assertIn("verify_hermes_openviking_target", runner) + self.assertIn('if [[ "$SUITE_LABEL" != "e2e" ]]', runner) + self.assertIn("HERMES_URL", runner) + self.assertIn("OPENVIKING_URL", runner) + self.assertIn("X-Hermes-Session-Id", runner) + self.assertIn("/api/v1/sessions/", runner) + self.assertIn("pending_tokens", runner) + self.assertIn("message_count", runner) + self.assertIn("Hermes completed the probe, but the configured OpenViking target did not receive it", runner) + self.assertIn("requests.delete", runner) + + def test_hermes_eval_modes_and_required_services_are_documented(self): + readme = read_text("benchmark/locomo/hermes/README.md") + + for suite in ["native", "e2e", "preingest"]: + self.assertRegex(readme, rf"`{re.escape(suite)}`|--suite {re.escape(suite)}") + self.assertIn("Hermes gateway running at `HERMES_URL`", readme) + self.assertIn("OpenViking server running at `OPENVIKING_URL`", readme) + self.assertIn("The default OpenViking benchmark setup is local and does not require an API key", readme) + + +class TestOvtestPluginFlow(unittest.TestCase): + def test_runner_and_docs_expose_plugin_contract_flow(self): + runner = read_text("tests/oc2ov_test/run_tests.py") + shell_runner = read_text("tests/oc2ov_test/run.sh") + docs = read_text("tests/oc2ov_test/README.md") + + self.assertIn('"plugins"', runner) + self.assertIn("tests.plugins.test_agent_plugin_contracts", runner) + self.assertIn("--plugins", shell_runner) + self.assertIn("python run_tests.py --type plugins", shell_runner) + self.assertIn("插件契约测试", docs) + self.assertIn("python run_tests.py --type plugins", docs) + for phrase in [ + "npm run check && npm test", + "wrappers/openviking.js", + "/experimental/tool/ids", + "memwrite", + "memsearch", + "codeoutline", + "HERMES_URL", + "OPENVIKING_URL", + "--suite e2e", + ]: + self.assertIn(phrase, docs) + + +if __name__ == "__main__": + unittest.main()