diff --git a/cn/registry/ai-compatibility.json b/cn/registry/ai-compatibility.json index a0298847..8ff46116 100644 --- a/cn/registry/ai-compatibility.json +++ b/cn/registry/ai-compatibility.json @@ -1,26 +1,26 @@ { - "_verified": "2026-08-24. level=official-doc: 官方集成文档存在,Adapter 于 M3-4 落地; level=stable 仅在 M3-8 mock 回归通过后授予", + "_verified": "2026-08-24. level=stable: adapter 渲染由 test/shell.d/omarchycn-ai-test.sh mock 回归覆盖; level=official-doc: 官方集成文档存在,native CLI 自管配置", "combos": { "claude-code": { "deepseek": { - "level": "official-doc", + "level": "stable", "doc": "https://api-docs.deepseek.com/quick_start/agent_integrations/claude_code/", "adapter": true }, "kimi": { - "level": "official-doc", + "level": "stable", "doc": "https://www.kimi.com/code/docs/en/third-party-tools/claude-code", "adapter": true }, "zai": { - "level": "official-doc", + "level": "stable", "doc": "https://docs.z.ai/devpack/tool/claude", "adapter": true } }, "codex": { "deepseek": { - "level": "official-doc", + "level": "stable", "doc": "https://api-docs.deepseek.com/quick_start/agent_integrations/codex/", "models": [ "deepseek-v4-flash", @@ -33,7 +33,7 @@ }, "opencode": { "deepseek": { - "level": "official-doc", + "level": "stable", "doc": "https://api-docs.deepseek.com/quick_start/agent_integrations/opencode/", "adapter": true }, diff --git a/test/shell.d/omarchycn-ai-test.sh b/test/shell.d/omarchycn-ai-test.sh new file mode 100755 index 00000000..43d48e65 --- /dev/null +++ b/test/shell.d/omarchycn-ai-test.sh @@ -0,0 +1,133 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname -- "${BASH_SOURCE[0]}")/base-test.sh" + +require_command jq +require_command python3 + +export OMARCHY_PATH="$ROOT" +export PATH="$ROOT/bin:$PATH" + +TEST_HOME=$(mktemp -d) +MOCK_PID="" +cleanup() { + [[ -n $MOCK_PID ]] && kill "$MOCK_PID" 2>/dev/null + rm -rf "$TEST_HOME" +} +trap cleanup EXIT +export HOME="$TEST_HOME" + +# Registries parse and carry the P0 providers and harnesses +for reg in ai-providers ai-harnesses ai-compatibility; do + jq -e . "$ROOT/cn/registry/$reg.json" > /dev/null || fail "$reg.json parses" +done +for p in deepseek kimi zai; do + jq -e --arg p "$p" '.providers[$p].endpoints.anthropic // .providers[$p].endpoints.openai' \ + "$ROOT/cn/registry/ai-providers.json" > /dev/null || fail "provider $p has an endpoint" +done +pass "AI registries parse with P0 providers" + +# Secret file backend roundtrip with 0600 +echo "sk-secret-1" | omarchy-cn-ai-secret set deepseek > /dev/null +[[ $(omarchy-cn-ai-secret get deepseek) == "sk-secret-1" ]] || fail "secret roundtrip" +[[ $(stat -c %a "$HOME/.local/state/omarchycn/secrets/deepseek") == 600 ]] || fail "secret file 0600" +pass "secret backend file fallback stores and returns the key at 0600" + +# Profile validation matrix +omarchy-cn-ai-profile-create good claude-code deepseek default-coding > /dev/null +omarchy-cn-ai-profile-create cdx codex deepseek deepseek-v4-flash > /dev/null +omarchy-cn-ai-profile-create ock opencode deepseek default-coding > /dev/null +if omarchy-cn-ai-profile-create bad codex kimi fast > /dev/null 2>&1; then + fail "unsupported combo rejected" +fi +if omarchy-cn-ai-profile-create bad claude-code kimi kimi-k2.6 > /dev/null 2>&1; then + fail "protocol-mismatched model rejected" +fi +if omarchy-cn-ai-profile-create bad codex deepseek "deepseek-v4-pro[1m]" > /dev/null 2>&1; then + fail "codex rejects provider-namespace slug" +fi +pass "profile validation enforces combos, protocols, and allowlists" + +# claude-code env render for all three providers, exact endpoints +source "$ROOT/cn/lib/ai.sh" +declare -A bases=( + [deepseek]="https://api.deepseek.com/anthropic" + [kimi]="https://api.kimi.com/coding/" + [zai]="https://api.z.ai/api/anthropic" +) +for p in deepseek kimi zai; do + out=$(cn_ai_render_env claude-code "$p" test-model sk-x) + grep -qF "ANTHROPIC_BASE_URL=${bases[$p]}" <<<"$out" || fail "claude-code $p base url" "$out" + grep -q "ANTHROPIC_AUTH_TOKEN=sk-x" <<<"$out" || fail "claude-code $p auth token" + grep -q "ANTHROPIC_API_KEY=sk-x" <<<"$out" || fail "claude-code $p api key" +done +out=$(cn_ai_render_env opencode deepseek test-model sk-x) +grep -q "DEEPSEEK_API_KEY=sk-x" <<<"$out" || fail "opencode deepseek env" +if cn_ai_render_env opencode zai m k > /dev/null 2>&1; then + fail "opencode zai refused" +fi +pass "env adapters render official endpoints for every stable combo" + +# codex config render: official contract, user content preserved, idempotent +mkdir -p "$HOME/.codex" +printf '[mcp_servers.x]\ncommand = "npx"\n' > "$HOME/.codex/config.toml" +cn_ai_render_codex_config deepseek deepseek-v4-flash sk-x +for want in 'model = "deepseek-v4-flash"' 'model_catalog_json = "~/.codex/models.json"' \ + 'preferred_auth_method = "apikey"' 'forced_login_method = "api"' \ + 'wire_api = "responses"' '[model_providers.deepseek]'; do + grep -qF "$want" "$HOME/.codex/config.toml" || fail "codex contract: $want" +done +grep -q "mcp_servers.x" "$HOME/.codex/config.toml" || fail "codex user section preserved" +[[ -f $HOME/.codex/models.json ]] || fail "codex models catalog installed" +cn_ai_render_codex_config deepseek deepseek-v4-flash sk-x +[[ $(grep -c "OmarchyCN ai model begin" "$HOME/.codex/config.toml") == 1 ]] || fail "codex render idempotent" +pass "codex adapter implements the official DeepSeek contract" + +# Mock server regression: anthropic 200/401 and codex responses path +port=$(python3 -c 'import socket; s = socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()') +python3 - "$port" <<'PYEOF' & +import http.server, sys +class H(http.server.BaseHTTPRequestHandler): + def do_POST(self): + self.rfile.read(int(self.headers.get("Content-Length", 0))) + key = self.headers.get("x-api-key") or (self.headers.get("Authorization") or "").replace("Bearer ", "") + if key != "sk-secret-1": + code = 401 + elif self.path in ("/v1/messages", "/responses", "/chat/completions"): + code = 200 + else: + code = 404 + self.send_response(code) + self.send_header("content-length", "2") + self.end_headers() + self.wfile.write(b"{}") + def log_message(self, *a): + pass +http.server.HTTPServer(("127.0.0.1", int(sys.argv[1])), H).serve_forever() +PYEOF +MOCK_PID=$! +for _ in $(seq 1 20); do + curl -sf -o /dev/null -X POST "http://127.0.0.1:$port/v1/messages" -H "x-api-key: sk-secret-1" && break + sleep 0.2 +done + +OMARCHYCN_AI_TEST_BASE_URL="http://127.0.0.1:$port" omarchy-cn-ai-test good | grep -q "PASS" || fail "anthropic mock 200" +OMARCHYCN_AI_TEST_BASE_URL="http://127.0.0.1:$port" omarchy-cn-ai-test cdx | grep -q "PASS" || fail "codex responses mock 200" +echo "wrong-key" | omarchy-cn-ai-secret set deepseek > /dev/null +if OMARCHYCN_AI_TEST_BASE_URL="http://127.0.0.1:$port" omarchy-cn-ai-test good > /dev/null 2>&1; then + fail "anthropic mock 401 fails the test" +fi +pass "mock regression: 200 pass, 401 fail, responses path exercised" + +# ai-default merges into claude settings without clobbering user config +echo "sk-secret-1" | omarchy-cn-ai-secret set deepseek > /dev/null +mkdir -p "$HOME/.claude" +printf '{"permissions":{"allow":["Bash"]},"env":{"KEEP":"1"}}' > "$HOME/.claude/settings.json" +omarchy-cn-ai-default good > /dev/null +[[ $(jq -r '.env.KEEP' "$HOME/.claude/settings.json") == 1 ]] || fail "ai-default preserves user env" +[[ $(jq -r '.permissions.allow[0]' "$HOME/.claude/settings.json") == Bash ]] || fail "ai-default preserves permissions" +[[ $(jq -r '.env.ANTHROPIC_BASE_URL' "$HOME/.claude/settings.json") == "https://api.deepseek.com/anthropic" ]] || fail "ai-default env written" +[[ $(cat "$HOME/.config/omarchy/defaults/agent") == claude ]] || fail "upstream default agent set" +pass "ai-default maps the profile onto the upstream agent entry"